Entender correctamente como funcionan las transacciones es importantísimo en Firebird porque todo lo haces dentro de una transacción, no existe alguna operación que pueda realizarse afuera de ellas. Eso significa que siempre todos tus INSERT, UPDATE, DELETE, FETCH, SELECT y EXECUTE PROCEDURE se ejecutan dentro de una transacción, sí o sí.

¿Por qué eso?

Porque de esta manera existe la seguridad 100% de que tu Base de Datos cumpla con ACID, que es un paradigma que todos los buenos SGBDR (y por lo tanto todas sus bases de datos) deben cumplir.

Las bases de datos que cumplen con ACID son totalmente confiables y todas las bases de datos de Firebird cumplen con ACID al 100%, sin excepción.

https://firebird21.wordpress.com/2013/05/10/entendiendo-acid/

Iniciando una transacción

Si no había una transacción abierta entonces una transacción se inicia automáticamente cuando escribes un comando (por ejemplo: INSERT, UPDATE, DELETE, SELECT) .

El comando SET TRANSACTION (que verás más abajo) no inicia la transacción, lo que hace es:

  1. Un COMMIT a la transacción actual (el cual, como todo COMMIT, puede finalizar exitosamente o fallar)
  2. Establecer los parámetros que serán usados por la siguiente transacción

Finalizando una transacción

Hay solamente dos formas (normales, porque también hay formas anormales) en que una transacción puede ser finalizada:

  • Con un COMMIT
  • Con un ROLLBACK

Si finaliza con un COMMIT entonces todo lo que se hizo dentro de la transacción queda guardado en las tablas de la Base de Datos.

Si finaliza con un ROLLBACK entonces todo lo que se hizo dentro de la transacción es desechado, eliminado, como si nunca se hubiera hecho.

Un COMMIT puede fallar, un ROLLBACK jamás falla.

En los ejemplos que hay más abajo puedes ver casos de COMMITs que fallan.

Sintaxis de SET TRANSACTION:

SET TRANSACTION
   [NAME NombreTransacción]
[READ WRITE | READ ONLY]
[ [ISOLATION LEVEL] { SNAPSHOT [TABLE STABILITY]
| READ COMMITTED [[NO] RECORD_VERSION] } ]
[WAIT | NO WAIT]
[LOCK TIMEOUT segundos]
[NO AUTO UNDO]
[IGNORE LIMBO]
[RESERVING | USING ]

NAME se usa cuando abres una transacción dentro de tu programa. Te sirve para asignarle un nombre a la transacción. De esa manera podrías tener varias transacciones abiertas al mismo tiempo y realizar operaciones independientes en cada una de ellas. Es opcional, si no quieres no lo usas pero si no lo usas entonces solamente puedes tener una transacción abierta.

Con READ WRITE le pides al Firebird que abra la transacción para lectura y para escritura. Es el parámetro por defecto. Y es el que debes utilizar si dentro de la transacción tendrás los comandos INSERT o UPDATE o DELETE. Con READ ONLY le pides al Firebird que abra la transacción para lectura solamente. Y es el que debes utilizar cuando dentro de tu transacción solamente tendrás el comando SELECT o el comando EXECUTE PROCEDURE y éste no realiza ningún INSERT ni UPDATE ni DELETE. Usar READ ONLY en una transacción que no modifica la Base de Datos hace que ésta finalice más rápido y eso es algo muy bueno.

ISOLATION LEVEL es el nivel de aislamiento de la transacción y puede tener uno de estos valores:

SNAPSHOT. Significa que la transacción solamente conoce los valores que fueron confirmados (se confirma con un COMMIT) antes de que ella empezara. En general se usa este nivel de aislamiento cuando la transacción solamente tendrá SELECTs.

SNAPSHOT TABLE STABILITY. Obtiene acceso exclusivo para escritura en todas las tablas involucradas y también en todas las tablas relacionadas con las anteriores mediante una Foreign Key. Ninguna otra transacción podrá modificar (insertar, actualizar, borrar) las tablas que una transacción SNAPSHOT TABLE STABILITY está usando. Y una transacción SNAPSHOT TABLE STABILITY no podrá iniciar si alguna de las tablas que necesita está siendo usada por otra transacción READ WRITE. En Firebird no es recomendable usar este aislamiento. Es muy peligroso porque impide que otras transacciones modifiquen las tablas. En el rarísimo caso de que debas usar este aislamiento trata de que la transacción termine muy rápido o de que se ejecute cuando nadie más usa la Base de Datos.

READ COMMITED. Permite que la transacción pueda “ver” todas las filas que fueron insertadas, actualizadas o borradas por otras transacciones y que fueron confirmadas (mediante un COMMIT) por esas otras transacciones. Es el aislamiento que normalmente se usa en los programas que insertan, modifican o borran filas de las tablas. Este aislamiento además puede ser RECORD_VERSION o NO RECORD_VERSION.

RECORD VERSION. En este caso la transacción puede leer todas las filas que fueron confirmadas por otras transacciones. Y si la transacción es READ WRITE entonces podrá modificar también esas filas pero siempre y cuando haya empezado después que las otras. Ejemplo:

      • En la tabla PRODUCTOS el precio de venta de “Televisor Toshiba de 20 pulgadas” es de 120 dólares
      • Empezó la transacción T1
      • Empezó la transacción T2
      • La transacción T1 cambió el precio de venta de “Televisor Toshiba de 20 pulgadas” a 100 dólares
      • La transacción T2 ve que el precio de venta de “Televisor Toshiba de 20 pulgadas” es de 120 dólares. Como la transacción T1 aún no finalizó con un COMMIT, la transacción T2 ve el precio que ese producto tenía cuando empezó la transacción T2. La transacción T2 no puede saber lo que hace la transacción T1 antes de que la transacción T1 finalice con un COMMIT.
      • La transacción T1 finalizó con un COMMIT
      • La transacción T2 cambió el precio de venta de “Televisor Toshiba de 20 pulgadas” a 110 dólares
      • La transacción T2 finalizó con un COMMIT
      • Ahora, el precio de venta es de 110 dólares porque es el que corresponde al último COMMIT

Como la transacción T2 había empezado después que la transacción T1 entonces el COMMIT de la transacción T2 fue exitoso. Si hubiera empezado antes, hubiera sido rechazado. Por ejemplo:

      • En la tabla PRODUCTOS el precio de venta de “Televisor Toshiba de 20 pulgadas” es de 120 dólares
      • Empezó la transacción T1
      • Empezó la transacción T2
      • La transacción T2 cambió el precio de venta de “Televisor Toshiba de 20 pulgadas” a 110 dólares
      • La transacción T1 ve que el precio de venta de “Televisor Toshiba de 20 pulgadas” es de 120 dólares. Como la transacción T2 aún no finalizó con un COMMIT, la transacción T1 ve el precio que ese producto tenía cuando empezó la transacción T1.  La transacción T1 no puede saber lo que hace la transacción T2 antes de que la transacción T2 finalice con un COMMIT.
      • La transacción T2 finalizó con un COMMIT
      • La transacción T1 cambió el precio de venta de “Televisor Toshiba de 20 pulgadas” a 100 dólares
      • La transacción T1 intenta finalizar con un COMMIT
      • Su intento de COMMIT es rechazado. Como la transacción T1 empezó antes que la transacción T2 entonces la transacción T1 no puede modificar una fila que fue modificada por la transacción T2, ya que la transacción T2 es más nueva
      • Ahora, el precio de venta es de 110 dólares, que corresponde al último COMMIT exitoso

NO RECORD_VERSION. Es el valor por defecto. Impide que la transacción pueda siquiera leer una fila que fue actualizada por otra transacción y que aún no fue confirmada. Por ejemplo:

      • En la tabla PRODUCTOS el precio de venta de “Televisor Toshiba de 20 pulgadas” es de 120 dólares
      • Empezó la transacción T1
      • Empezó la transacción T2
      • La transacción T1 cambió el precio de venta de “Televisor Toshiba de 20 pulgadas” a 100 dólares
      • La transacción T2 no puede ver cual es el precio de venta de “Televisor Toshiba de 20 pulgadas” porque la transacción T1 aún no finalizó con un COMMIT. Por lo tanto la transacción T2 no tiene acceso a esa fila, ni siquiera para lectura. Y ahora pueden ocurrir dos cosas:
        1. Si el modo de bloqueo de la transacción T2 es WAIT entonces la transacción T2 esperará hasta que la transacción T1 finalice con un COMMIT o con un ROLLBACK. En ambos casos, como la transacción T2 es más nueva que la transacción T1 entonces la transacción T2 podrá finalizar con un COMMIT
        2. Si el modo de bloqueo de la transacción T2 es NO WAIT entonces la transacción T2 recibirá una notificación de que la fila que quiso modificar está bloqueada
      • Si la transacción T1 finaliza con un COMMIT, entonces el precio de venta será de 100 dólares, si finaliza con un ROLLBACK el precio de venta continuará en 120 dólares

WAIT. Este es el modo de bloqueo por defecto. Sirve para lo siguiente: cuando una transacción no puede modificar o borrar una fila porque otra transacción está usando esa fila, espera hasta que la otra transacción termine. Si la transacción que estaba esperando es más nueva entonces podrá modificar a esa fila cuando sea desbloqueada. Si es más vieja, obtendrá inmediatamente una notificación de error. No hay que usar WAIT en programas que muchos usuarios utilizan para modificar o borrar las mismas filas porque puede causar que muchos usuarios estén esperando varios minutos antes de poder continuar con sus tareas. Tampoco tiene sentido usar WAIT en transacciones SNAPSHOT sin usar también LOCK TIMEOUT en ellas. De todas maneras hay excepciones y lo que puedes hacer es verificar si en tu caso es bueno usar o no usar WAIT. En general, si son pocos los usuarios que modifican las mismas filas y las transacciones son cortas, usar WAIT suele ser lo correcto. Cuando el modo de bloqueo es NOWAIT el Servidor del Firebird inmediatamente informa que ocurrió un conflicto porque una transacción intenta modificar una fila que otra transacción también está modificando. Si muchos usuarios están modificando las mismas filas NOWAIT les informará rápidamente que ocurrió un problema mientras que WAIT les hará esperar. En transacciones SNAPSHOT lo recomendable suele ser usar NOWAIT y cuando se encuentra un conflicto hacer un ROLLBACK o esperar unos segundos y reintentar la modificación.

LOCK TIMEOUT es muy útil para evitar que una transacción espere indefinidamente que la fila sea desbloqueada. Solamente se puede usar cuando el modo de bloqueo es WAIT. Por ejemplo: WAIT LOCK TIMEOUT 5 esperará un máximo de 5 segundos para que la fila sea desbloqueada. Si no fue desbloqueada entonces se recibirá una notificación, como si el modo de bloqueo hubiera sido NOWAIT. Lastimosamente no puede usarse en transacciones iniciadas dentro de un programa.

NO AUTO UNDO le indica al Servidor del Firebird que no guarde los datos que normalmente guarda para hacer el ROLLBACK de la transacción. Si tu transacción tendrá muchos INSERT y estás seguro que no finalizará con un ROLLBACK entonces con esta opción conseguirás que la transacción finalice más rápidamente.

IGNORE LIMBO. Con esta opción los registros creados por las transacciones limbo son ignorados. Una transacción está en limbo si la segunda etapa de un COMMIT de dos etapas falla. Los COMMIT de dos etapas se usan en transacciones que involucran a dos bases de datos. No se usa esta opción en transacciones que son iniciadas dentro de los programas.

RESERVING reserva todas las tablas cuyos nombres se encuentran a continuación. La reserva empieza cuando la transacción empieza y termina cuando la transacción finaliza. Eso le garantiza a la transacción tener acceso a todas esas tablas y ninguna otra transacción podrá impedirle el acceso a las tablas listadas. SNAPSHOT TABLE STABILITY bloquea a todas las tablas que están dentro de la transacción (si hay 7 tablas bloquea a las 7), en cambio RESERVING solamente reserva a las tablas cuyos nombres se especificaron (quizás de esas 7 solamente 2 necesitaban realmente ser reservadas), siendo por lo tanto menos restrictivo y el método preferible cuando se deben bloquear o reservar tablas (algo que en Firebird muy raramente debería hacerse). USING limita los nombres de las bases de datos a los cuales la transacción puede acceder a los que aquí se hayan especificado.

Opciones por defecto

Las opciones por defecto de las transacciones (o sea, las que usará el Servidor del Firebird si no le indicas otra cosa) son:

READ WRITE + WAIT + SNAPSHOT

Lecturas sucias

Firebird no permite las lecturas sucias. Se le llama lectura sucia a los cambios que hace una transacción y antes de que dicha transacción termine con un COMMIT desde otra transacción se pueden ver esos cambios. Por ejemplo: la transacción T1 está haciendo un INSERT y antes de que la transacción T1 finalice con un COMMIT desde la transacción T2 ya se está viendo ese INSERT que hizo la transacción T1. Otros SGBDR permiten esa situación, pero Firebird no. Y es lo correcto porque eso otorga mucha mayor seguridad.

NOTA:

Todas las transacciones siempre pueden ver a todos los registros que fueron confirmados (finalizaron con un COMMITantes de que ellas empezaran. Pero solamente las transacciones READ COMMITED pueden ver a los registros que fueron confirmados después de que ellas empezaran.

Artículos relacionados:

COMMIT y ROLLBACK en stored procedures y triggers

Detectando aplicaciones y usuarios que mantienen las transacciones abiertas durante mucho tiempo

Terminar las transacciones de los SELECTs

Modos de bloqueo de las transacciones

Bloqueos mortales

Entendiendo ACID

La arquitectura MGA

Transacciones optimistas y transacciones pesimistas

El índice del blog Firebird21

El foro del blog Firebird21