该页面演示了如何使用触发器为使用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命名空间的结尾
}