Esta página muestra cómo usar disparadores para implementar la lógica de deshacer / rehacer para una aplicación que usa SQLite como formato de archivo de la aplicación.

Diseño orientado a objetos

Esta nota de diseño considera que la base de datos es una colección de objetos. Cada tabla SQL es una clase. Cada fila es una instancia de esa clase. Por supuesto, existen otras formas de interpretar un esquema de base de datos SQL, y las técnicas descritas aquí funcionan igualmente bien bajo interpretaciones alternativas, pero una vista orientada a objetos parece ser más natural para la mayoría de los programadores contemporáneos.

Capturar cambios mediante disparadores

La idea central es crear una tabla especial (llamada “UNDOLOG” en el ejemplo) que contenga la información necesaria para deshacer / rehacer cambios en la base de datos. Para cada clase (tabla) en la base de datos que quiere participar en deshacer / rehacer, se crean disparadores que hacen que se realicen entradas en la tabla UNDOLOG para cada DELETE, INSERT y UPDATE de la clase participante. Las entradas UNDOLOG consisten en sentencias SQL ordinarias que se pueden reproducir para revertir los cambios.

Por ejemplo, suponga que desea deshacer / rehacer en una clase (tabla) que se ve así:

CREATETABLE ex1(a,b,c);

Los activadores para registrar cambios en la tabla EX1 podrían tener este aspecto:

CREATETEMPTRIGGER ex1_it AFTERINSERTON ex1 BEGININSERTINTO undolog VALUES(NULL,'DELETE FROM ex1 WHERE rowid='||new.rowid);END;CREATETEMPTRIGGER ex1_ut AFTERUPDATEON ex1 BEGININSERTINTO undolog VALUES(NULL,'UPDATE ex1
     SET a='||quote(old.a)||',b='||quote(old.b)||',c='||quote(old.c)||'
   WHERE rowid='||old.rowid);END;CREATETEMPTRIGGER ex1_dt BEFORE DELETEON ex1 BEGININSERTINTO undolog VALUES(NULL,'INSERT INTO ex1(rowid,a,b,c)
    VALUES('||old.rowid||','||quote(old.a)||','||quote(old.b)||','||quote(old.c)||')');END;

Después de cada INSERT en ex1, el disparador ex1_it construye el texto de una instrucción DELETE que deshará el INSERT. El disparador ex1_ut construye una instrucción UPDATE que deshará los efectos de una UPDATE. Y el disparador ex1_dt construye una declaración que deshará los efectos de un DELETE.

Tenga en cuenta el uso de la función SQL quote () en estos disparadores. La función quote () convierte su argumento en una forma apropiada para su inclusión en una declaración SQL. Los valores numéricos no se modifican. Las comillas simples se agregan antes y después de las cadenas y las comillas simples internas se escapan. Los valores BLOB se representan utilizando la notación BLOB hexadecimal estándar de SQL. El uso de la función quote () asegura que las sentencias SQL utilizadas para deshacer y rehacer estén siempre a salvo de la inyección SQL.

Creación automática de disparadores

Los activadores como los anteriores se pueden ingresar manualmente, pero eso es tedioso. Una característica importante de la técnica que se muestra a continuación es que los disparadores se generan automáticamente.

El lenguaje de implementación para el código de ejemplo es TCL, aunque puede hacer lo mismo fácilmente en otro lenguaje de programación. Recuerde que el código aquí es una demostración de la técnica, no un módulo desplegable que automáticamente hará todo por usted. El código de demostración que se muestra a continuación se deriva del código real en uso de producción. Pero deberá realizar cambios para adaptarlo a su aplicación.

Para activar la lógica de deshacer / rehacer, invoque el comando undo :: activar con todas las clases (tablas) que van a participar en el deshacer / rehacer como argumentos. Utilice undo :: deactivate, undo :: freeze y undo :: undozeze para controlar el estado del mecanismo de deshacer / rehacer.

El comando undo :: activar crea disparadores temporales en la base de datos que registran todos los cambios realizados en las tablas nombradas en los argumentos.

Interfaz de aplicación

Después de una secuencia de cambios que definen un solo paso deshacer / rehacer, invoque el comando undo :: barrera para definir el límite de ese paso. En un programa interactivo, puede llamar a undo :: event después de cualquier cambio y se llamará a undo :: barrera automáticamente como una devolución de llamada inactiva.

Cuando el usuario presiona el botón Deshacer, invoca deshacer :: deshacer. Invocar deshacer :: rehacer cuando el usuario presiona el botón Rehacer.

En cada llamada a undo :: undo o undo :: redo, el módulo undo / redo invoca automáticamente los métodos status_refresh y reload_all en todos los espacios de nombres de nivel superior. Estos métodos deben definirse para reconstruir la pantalla o actualizar el estado del programa en función de los cambios deshechos / rehechos en la base de datos.

El código de demostración a continuación incluye un método status_refresh que se atenúa o activa los botones Deshacer y Rehacer y las entradas del menú dependiendo de si hay algo que deshacer o rehacer. Deberá redefinir este método para controlar los botones Deshacer y Rehacer en su aplicación.

El código de demostración asume que la base de datos SQLite se abre y se utiliza como un objeto de base de datos llamado “db”.

Código de ejemplo

# Everything goes in a private namespace
namespace eval ::undo 

# proc:  ::undo::activate TABLE ...# title: Start up the undo/redo system## Arguments should be one or more database tables (in the database associated# with the handle "db") whose changes are to be recorded for undo/redo# purposes.#proc activate args 
  variable _undo
  if $_undo(active) return
  eval _create_triggers db $args
  set _undo(undostack) 
  set _undo(redostack) 
  set _undo(active)1set _undo(freeze)-1
  _start_interval


# proc:  ::undo::deactivate# title: Halt the undo/redo system and delete the undo/redo stacks#proc deactivate  
  variable _undo
  if !$_undo(active) return
  _drop_triggers db
  set _undo(undostack) 
  set _undo(redostack) 
  set _undo(active)0set _undo(freeze)-1


# proc:  ::undo::freeze# title: Stop accepting database changes into the undo stack## From the point when this routine is called up until the next unfreeze,# new database changes are rejected from the undo stack.#proc freeze  
  variable _undo
  if !; hd_resolve_one info exists _undo(freeze); hd_puts  returnif $_undo(freeze)>=0 error "recursive call to ::undo::freeze"
  set _undo(freeze) ; hd_resolve_one db one SELECTcoalesce(max(seq),0)FROM undolog; hd_puts 


# proc:  ::undo::unfreeze# title: Begin accepting undo actions again.#proc unfreeze  
  variable _undo
  if !; hd_resolve_one info exists _undo(freeze); hd_puts  returnif $_undo(freeze)<0 error "called ::undo::unfreeze while not frozen"
  db eval "DELETE FROM undolog WHERE seq>$_undo(freeze)"set _undo(freeze)-1


# proc:  ::undo::event# title: Something undoable has happened## This routine is called whenever an undoable action occurs.  Arrangements# are made to invoke ::undo::barrier no later than the next idle moment.#proc event  
  variable _undo
  if $_undo(pending)=="" 
    set _undo(pending) ; hd_resolve_one after idle ::undo::barrier; hd_puts 
  


# proc:  ::undo::barrier# title: Create an undo barrier right now.#proc barrier  
  variable _undo
  catch after cancel $_undo(pending)
  set _undo(pending) 
  if !$_undo(active) 
    refresh
    return
  
  setend ; hd_resolve_one db one SELECTcoalesce(max(seq),0)FROM undolog; hd_puts 
  if $_undo(freeze)>=0&& $end>$_undo(freeze) setend $_undo(freeze)
  setbegin $_undo(firstlog)
  _start_interval
  if $begin==$_undo(firstlog) 
    refresh
    return
  
  lappend _undo(undostack) ; hd_resolve_one list $begin $end; hd_puts 
  set _undo(redostack) 
  refresh


# proc:  ::undo::undo# title: Do a single step of undo#proc undo  
  _step undostack redostack


# proc:  ::undo::redo# title: Redo a single step#proc redo  
  _step redostack undostack


# proc:   ::undo::refresh# title:  Update the status of controls after a database change## The undo module calls this routine after any undo/redo in order to# cause controls gray out appropriately depending on the current state# of the database.  This routine works by invoking the status_refresh# module in all top-level namespaces.#proc refresh  
  set body 
  foreach ns ; hd_resolve_one namespace children ::; hd_puts  
    if ; hd_resolve_one info proc $ns::status_refresh; hd_puts =="" continue
    append body $ns::status_refreshn
  
  proc ::undo::refresh  $body
  refresh


# proc:   ::undo::reload_all# title:  Redraw everything based on the current database## The undo module calls this routine after any undo/redo in order to# cause the screen to be completely redrawn based on the current database# contents.  This is accomplished by calling the "reload" module in# every top-level namespace other than ::undo.#proc reload_all  
  set body 
  foreach ns ; hd_resolve_one namespace children ::; hd_puts  
    if ; hd_resolve_one info proc $ns::reload; hd_puts =="" continue
    append body $ns::reloadn
  
  proc ::undo::reload_all  $body
  reload_all


############################################################################### The public interface to this module is above.  Routines and variables that# follow (and whose names begin with "_") are private to this module.############################################################################### state information#set _undo(active)0set _undo(undostack) 
set _undo(redostack) 
set _undo(pending) 
set _undo(firstlog)1set _undo(startstate) 


# proc:  ::undo::status_refresh# title: Enable and/or disable menu options a buttons#proc status_refresh  
  variable _undo
  if  ; hd_resolve_one llength $_undo(undostack); hd_puts ==0 
    .mb.edit entryconfig Undo -state disabled
    .bb.undo config -state disabled
   else 
    .mb.edit entryconfig Undo -state normal
    .bb.undo config -state normal
  
  if !$_undo(active); hd_resolve_one llength $_undo(redostack); hd_puts ==0 
    .mb.edit entryconfig Redo -state disabled
    .bb.redo config -state disabled
   else 
    .mb.edit entryconfig Redo -state normal
    .bb.redo config -state normal
  


# xproc:  ::undo::_create_triggers DB TABLE1 TABLE2 ...# title:  Create change recording triggers for all tables listed## Create a temporary table in the database named "undolog".  Create# triggers that fire on any insert, delete, or update of TABLE1, TABLE2, ....# When those triggers fire, insert records in undolog that contain# SQL text for statements that will undo the insert, delete, or update.#proc _create_triggers db args 
  catch $db eval DROPTABLE undolog
  $db eval CREATETEMPTABLE undolog(seq integerprimarykey,sqltext)
  foreach tbl $args 
    set collist ; hd_resolve_one $db eval "pragma table_info($tbl)"; hd_puts 


# xproc:  ::undo::_drop_triggers DB# title:  Drop all of the triggers that _create_triggers created#proc _drop_triggers db 
  set tlist ; hd_resolve_one $db eval SELECT name FROM sqlite_temp_schema
                       WHEREtype='trigger'; hd_puts 
  foreach trigger $tlist 
    if !; hd_resolve_one regexp _.*_(i $trigger; hd_puts  continue
    $db eval "DROP TRIGGER $trigger;"
  
  catch $db eval DROPTABLE undolog


# xproc: ::undo::_start_interval# title: Record the starting conditions of an undo interval#proc _start_interval  
  variable _undo
  set _undo(firstlog) ; hd_resolve_one db one SELECTcoalesce(max(seq),0)+1FROM undolog; hd_puts 


# xproc: ::undo::_step V1 V2# title: Do a single step of undo or redo## For an undo V1=="undostack" and V2=="redostack".  For a redo,# V1=="redostack" and V2=="undostack".#proc _step v1 v2 
  variable _undo
  set op ; hd_resolve_one lindex $_undo($v1)end; hd_puts 
  set _undo($v1) ; hd_resolve_one lrange $_undo($v1)0end-1; hd_puts 
  foreach beginend $op break
  db eval BEGINset q1 "SELECT sql FROM undolog WHERE seq>=$begin AND seq<=$end
          ORDER BY seq DESC"set sqllist ; hd_resolve_one db eval $q1; hd_puts 
  db eval "DELETE FROM undolog WHERE seq>=$begin AND seq<=$end"set _undo(firstlog) ; hd_resolve_one db one SELECTcoalesce(max(seq),0)+1FROM undolog; hd_puts 
  foreach sql $sqllist 
    db eval $sql
  
  db eval COMMIT
  reload_all

  setend ; hd_resolve_one db one SELECTcoalesce(max(seq),0)FROM undolog; hd_puts 
  setbegin $_undo(firstlog)
  lappend _undo($v2) ; hd_resolve_one list $begin $end; hd_puts 
  _start_interval
  refresh



# End of the ::undo namespace