该页面演示了如何使用触发器为使用SQLite作为其应用程序文件格式的应用程序实现撤消/重做逻辑 。
本设计说明将数据库视为对象的集合。每个SQL表都是一个类。每行都是该类的一个实例。当然,还有其他解释SQL数据库模式的方法,并且此处描述的技术在替代解释下也能很好地工作,但是对于大多数当代程序员而言,面向对象的观点似乎更为自然。
核心思想是创建一个特殊的表(在示例中命名为“ UNDOLOG”),该表包含撤消/重做对数据库所做的更改所需的信息。对于数据库中要参与撤消/重做的每个类(表),将创建触发器,这些触发器将导致在UNDOLOG表中为参与类的每个DELETE,INSERT和UPDATE进行输入。UNDOLOG条目由普通的SQL语句组成,可以对其进行回放以撤消更改。
例如,假设您要对如下所示的类(表)执行撤消/重做操作:
创建表ex1(a,b,c);
记录对表EX1的更改的触发器可能看起来像这样:
在EX1开始插入后创建温度触发ex1_it INSERT INTO undolog VALUES(NULL,'DELETE FROM ex1 WHERE rowid ='|| new.rowid); 结尾; 在EX1开始更新后创建温度触发ex1_ut INSERT INTO undolog VALUES(NULL,'UPDATE ex1 设置a ='|| quote(old.a)||',b ='|| quote(old.b)||',c ='|| quote(old.c)||' WHERE rowid ='|| old.rowid); 结尾; 开始删除ex1之前创建温度触发ex1_dt INSERT INTO undolog VALUES(NULL,'INSERT INTO ex1(rowid,a,b,c) VALUES('|| old.rowid ||','|| quote(old.a)||','|| quote(old.b)|| ','|| quote(old.c)||')'); 结尾;
在ex1上执行每个INSERT之后,ex1_it触发器将构造DELETE语句的文本,该文本将撤消INSERT。ex1_ut触发器构造一个UPDATE语句,该语句将撤消UPDATE的影响。并且ex1_dt触发器构造一个语句,该语句将撤消DELETE的影响。
注意在这些触发器中使用quote()SQL函数。quote()函数将其参数转换为适合包含在SQL语句中的形式。数值不变。在字符串前后添加单引号,并且对任何内部单引号进行转义。BLOB值使用SQL标准十六进制BLOB表示法呈现。使用quote()函数可确保用于撤消和重做的SQL语句始终对SQL注入安全。
可以手动输入上述触发器,但这很繁琐。下面演示的技术的一个重要特征是触发器是自动生成的。
示例代码的实现语言是 TCL,尽管您可以轻松地用另一种编程语言来做同样的事情。请记住,这里的代码是该技术的演示,而不是将自动为您做所有事情的嵌入式模块。下面显示的演示代码源自生产中的实际代码。但是您将需要进行更改以使其适合您的应用程序。
要激活撤消/重做逻辑,请以所有要参与撤消/重做的类(表)作为参数来调用undo :: activate命令。使用undo :: deactivate,undo :: freeze和undo :: unfreeze控制撤消/重做机制的状态。
undo :: activate命令在数据库中创建临时触发器,该触发器记录对参数中命名的表所做的所有更改。
在定义单个撤消/重做步骤的一系列更改之后,调用undo :: barrier命令定义该步骤的限制。在交互式程序中,可以在进行任何更改后调用undo :: event,并且undo :: barrier会作为空闲回调自动调用。
当用户按下“撤消”按钮时,调用undo :: undo。当用户按下“重做”按钮时,调用undo :: redo。
在每次调用undo :: undo或undo :: redo时,撤消/重做模块都会在所有顶级名称空间中自动调用status_refresh和reload_all方法。应该定义这些方法,以根据对数据库的撤消/重做更改来重建显示或以其他方式更新程序的状态。
下面的演示代码包括一个status_refresh方法,该方法根据是否要撤消或重做任何操作而将其变灰或激活“撤消”和“重做”按钮和菜单项。您将需要重新定义此方法,以控制应用程序中的“撤消”和“重做”按钮。
该演示代码假定已打开SQLite数据库,并将其用作名为“ db”的数据库对象。
#一切都进入一个私有名称空间 命名空间eval :: undo { #proc::: undo :: activate TABLE ... #title:启动撤消/重做系统 # #参数应为一个或多个数据库表(在与数据库关联 #带有句柄“ db”),其更改将记录为撤消/重做 #目的。 # proc activate {args} { 变量_undo 如果{$ _undo(active)}返回 评估_create_triggers db $ args 设置_undo(undostack){} 设置_undo(redostack){} 设置_undo(active)1 设置_undo(freeze)-1 _start_interval } #proc::: undo :: deactivate #标题:停止撤消/重做系统并删除撤消/重做堆栈 # proc停用{} { 变量_undo 如果{!$ _ undo(active)}返回 _drop_triggers db 设置_undo(undostack){} 设置_undo(redostack){} 设置_undo(active)0 设置_undo(freeze)-1 } #proc::: undo :: freeze #title:停止接受数据库更改到撤消堆栈中 # #从调用此例程的那一刻起,直到下一次解冻为止, #新的数据库更改从撤消堆栈中被拒绝。 # 程序冻结{} { 变量_undo 如果 {!}; hd_resolve_one {信息存在_undo(冻结)}; hd_puts {}返回 如果{$ _undo(freeze)> = 0} {错误“对:: undo :: freeze的递归调用”} 设置_undo(freeze)}; hd_resolve_one {db一{SELECT合并(max(seq),0)来自undolog}}; hd_puts { } #proc::: undo :: unfreeze #title:再次开始接受撤消操作。 # proc解冻{} { 变量_undo 如果 {!}; hd_resolve_one {信息存在_undo(冻结)}; hd_puts {}返回 如果{$ _undo(freeze)<0} {错误“未冻结时称为:: undo :: unfreeze”} db eval“从撤消日志中删除seq> $ _ undo(freeze)” 设置_undo(freeze)-1 } #proc::: undo :: event #标题:发生了不可挽回的事情 # #每当发生不可撤消的动作时,都会调用此例程。安排 #不迟于下一个空闲时间调用:: undo :: barrier。 # proc事件{} { 变量_undo 如果{$ _undo(pending)==“”} { 设置_undo(pending)}; hd_resolve_one {闲置后:: undo :: barrier}; hd_puts { } } #proc::: undo :: barrier #title:立即创建撤消障碍。 # proc障碍{} { 变量_undo catch {取消$ _undo(待定)之后} 设置_undo(待定){} 如果{!$ _ undo(active)} { 刷新 返回 } 结束 hd_resolve_one {db一{SELECT合并(max(seq),0)来自undolog}}; hd_puts { 如果{$ _undo(freeze)> = 0 && $ end> $ _ undo(freeze)} {set end $ _undo(freeze)} 设置开始$ _undo(firstlog) _start_interval 如果{$ begin == $ _ undo(firstlog)} { 刷新 返回 } lappend _undo(undostack)}; hd_resolve_one {list $ begin $ end}; hd_puts { 设置_undo(redostack){} 刷新 } #proc::: undo :: undo #标题:撤消一步 # proc undo {} { _step undostack重做堆栈 } #proc::: undo :: redo #标题:重做一步 # proc redo {} { _step redostack undostack } #proc::: undo :: refresh #title:数据库更改后更新控件的状态 # #撤消模块在执行任何撤消/重做操作后调用此例程,以便 #使控件根据当前状态适当变灰 数据库编号。该例程通过调用status_refresh来工作 所有顶级名称空间中的#模块。 # proc刷新{} { 设置身体{} foreach ns}; hd_resolve_one {namespace children ::}; hd_puts {{ 如果 {}; hd_resolve_one {info proc $ {ns} :: status_refresh}; hd_puts {==“”}继续 附加正文$ {ns} :: status_refresh \ n } proc :: undo :: refresh {} $ body 刷新 } #proc::: undo :: reload_all #title:根据当前数据库重新绘制所有内容 # #撤消模块在执行任何撤消/重做操作后调用此例程,以便 #使屏幕根据当前数据库完全重绘 # 内容。这是通过在 #除了:: undo以外的每个顶级名称空间。 # proc reload_all {} { 设置身体{} foreach ns}; hd_resolve_one {namespace children ::}; hd_puts {{ 如果 {}; hd_resolve_one {info proc $ {ns} :: reload}; hd_puts {==“”}继续 附加正文$ {ns} :: reload \ n } proc :: undo :: reload_all {} $ body reload_all } ################################################ ########################### #此模块的公共接口在上面。例程和变量 #follow(其名称以“ _”开头)是该模块专用的。 ################################################ ########################### #状态信息 # 设置_undo(active)0 设置_undo(undostack){} 设置_undo(redostack){} 设置_undo(待定){} 设置_undo(firstlog)1 设置_undo(startstate){} #proc::: undo :: status_refresh #title:启用和/或禁用菜单选项a按钮 # proc status_refresh {} { 变量_undo 如果{!$ _ undo(active)|| }; hd_resolve_one {llength $ _undo(undostack)}; hd_puts {== 0} { .mb.edit entryconfig撤消状态已禁用 .bb.undo config -state已禁用 } 别的 { .mb.edit entryconfig撤消状态正常 .bb.undo config -state正常 } 如果{!$ _ undo(active)|| }; hd_resolve_one {llength $ _undo(redostack)}; hd_puts {== 0} { .mb.edit entryconfig重做状态已禁用 .bb.redo config -state已禁用 } 别的 { .mb.edit entryconfig重做状态正常 .bb.redo config -state正常 } } #xproc ::: undo :: _ create_triggers DB TABLE1 TABLE2 ... #title:为列出的所有表创建更改记录触发器 # #在数据库“ undolog”中创建一个临时表。创造 #在对TABLE1,TABLE2,...进行任何插入,删除或更新时触发的触发器。 #当这些触发器触发时,在undolog中插入包含以下内容的记录 #撤消插入,删除或更新的语句的SQL文本。 # proc _create_triggers {db args} { 捕获{$ db eval {DROP TABLE undolog}} $ db eval {创建温度表undolog(seq整数主键,sql文本)} foreach tbl $ args { 设置collist}; hd_resolve_one {$ db eval“ pragma table_info($ tbl)”}; hd_puts { 设置sql“ CREATE TEMP TRIGGER _ $ {tbl} _it在$ tbl开始时插入INSERT \ n” 追加sql“ INSERT INTO undolog VALUES(NULL,” 附加sql“'从$ tbl删除,其中rowid ='|| new.rowid); \ nEND; \ n” 附加sql“在$ tbl开始时,在CREATE TEMP TRIGGER _ $ {tbl} _ut之后更新\ n” 追加sql“ INSERT INTO undolog VALUES(NULL,” 附加sql“'UPDATE $ tbl” 设置Sep“ SET” foreach {x1名称x2 x3 x4 x5} $ collist { 附加sql“ $ sep $ name ='|| quote(old。$ name)||'” 设置sep“,” } 附加sql“ WHERE rowid ='|| old.rowid); \ nEND; \ n” 附加sql“在删除$ tbl开始之前,先创建温度触发器_ $ {tbl} _dt \ n” 追加sql“ INSERT INTO undolog VALUES(NULL,” 附加sql“'INSERT INTO $ {tbl}(rowid” foreach {x1名称x2 x3 x4 x5} $ collist {追加sql,$ name} 附加sql“)VALUES('|| old.rowid ||'” foreach {x1名称x2 x3 x4 x5} $ collist {追加sql,'|| quote(old。$ name)||''} 附加sql“)'); \ nEND; \ n” $ db评估$ sql } } #xproc ::: undo :: _ drop_triggers数据库 #title:删除_create_triggers创建的所有触发器 # proc _drop_triggers {db} { 设置tlist}; hd_resolve_one {$ db eval {从sqlite_temp_schema中选择名称 WHERE type ='trigger'}}; hd_puts { foreach触发器$ tlist { 如果 {!}; hd_resolve_one {regexp {_。* _(i | u | d)t $} $ trigger}; hd_puts {}继续 $ db eval“ DROP TRIGGER $ trigger;” } 捕获{$ db eval {DROP TABLE undolog}} } #xproc ::: undo :: _ start_interval #title:记录撤消间隔的开始条件 # proc _start_interval {} { 变量_undo 设置_undo(firstlog)}; hd_resolve_one {db一{SELECT Coalesce(max(seq),0)+1 FROM undolog}}; hd_puts { } #xproc::: undo :: _ step V1 V2 #title:执行撤消或重做的单个步骤 # #对于撤消V1 ==“ undostack”和V2 ==“ redostack”。要重做, #V1 ==“ redostack”和V2 ==“ undostack”。 # proc _step {v1 v2} { 变量_undo 设置op}; hd_resolve_one {lindex $ _undo($ v1)end}; hd_puts { 设置_undo($ v1)}; hd_resolve_one {lrange $ _undo($ v1)0 end-1}; hd_puts { foreach {开始结束} $ op中断 db eval开始 设置q1“从撤消日志中选择sql,其中seq> = $ begin AND seq <= $ end ORDER BY seq DESC” 设置sqllist}; hd_resolve_one {db eval $ q1}; hd_puts { db eval“从undolog中删除seq> = $ begin AND seq <= $ end” 设置_undo(firstlog)}; hd_resolve_one {db一{SELECT Coalesce(max(seq),0)+1 FROM undolog}}; hd_puts { foreach sql $ sqllist { db eval $ sql } 数据库评估提交 reload_all 结束 hd_resolve_one {db一{SELECT合并(max(seq),0)来自undolog}}; hd_puts { 设置开始$ _undo(firstlog) lappend _undo($ v2)}; hd_resolve_one {list $ begin $ end}; hd_puts { _start_interval 刷新 } #:: undo命名空间的结尾 }