Small. Fast. Reliable.
Choose any three.

使用SQLite自动撤消/重做

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