Las bases de datos se operan mediante sentencias, generalmente SQL, si la base de datos es relacional. Una sentencia es una operación que se aplica sobre uno o varios registros, y dicha sentencia puede tener éxito (modificándose la base de datos) o no (no se modifica la base de datos).
A diferencia de una sentencia, una transacción es un conjunto de sentencias diferentes que se envían a una base de datos y que se ejecutan en lote. Pero cualquiera de las sentencias pueden fallar, y es posible que no convenga que si falla una se guarde el resto.
Para evitar que esto ocurra, se suelen utilizar las sentencias begin transaction para indicar el inicio de la transacción, commit para confirmar los cambios si todo ha ido bien, y rollback, para descartar todos los cambios si se ha producido algún error.
Pero en Grails no existen estas sentencias, así que en este post voy a mostrar cómo podemos realizar una transacción que se confirme si todo va bien, o se rechace si hay algún problema.
Imaginemos que tenemos las siguientes clases de dominio (mapeadas a la base de datos, pero eso es otra historia):
class Pelicula{ String titulo Director director } class Director{ String nombre }
Si queremos insertar un par de películas con su director, pero queremos que se haga todo en una transacción de forma que si algo falla no se guarde nada, utilizaremos el siguiente código:
// Objeto sobre el que creamos la transacción def unaPelicula = new Pelicula() unaPelicula.withTransaction { status -> try { // Instanciamos y guardamos un par de directores... def director1 = new Director(nombre: "Ridley Scott") director1.save(flush:true, failOnError:true) def director2 = new Director(nombre: "Sylvester Stallone") director2.save(flush:true, failOnError:true) // Ahora instanciamos un par de películas // y las guardamos con su director def pelicula1 = new Pelicula(titulo: "Blade Runner", director: director1) pelicula1.save(flush:true, failOnError:true) def pelicula2 = new Pelicula(titulo: "Los Mercenarios", director: director2) pelicula2.save(flush:true, failOnError:true) }catch(Exception e){ // Si alguna linea peta, hacemos un rollback de todo! println "ERROR ---> ${e.getMessage()}" status.setRollbackOnly() } }
El truco está en instanciar un objeto de una de las clases de dominio, y sobre este objeto llamar al método withTransaction, que recibe como parámetro una closure (status), en la que aplicaremos toda la lógica de la transacción.
Dicha lógica estará dentro de un bloque try..catch, de forma que si se produce algún error el flujo se capture en el catch, donde tendremos la instrucción setRollbackOnly, que descartará todos los cambios que se hayan efectuado en el bloque try.
Por último, añadiremos en cada instrucción save el argumento failOnError:true de forma que si se produce algún problema, se lance una excepción que dirija el flujo al catch.
De esta manera, si todo va bien se producirá un commit, mientras que si hay algún error se descartará todo mediante un rollback.
withTransaction is a static method, so there’s no sense in creating a new instance of Pelicula and throwing it away. Just use «Pelicula.withTransaction { status -> … }»
Hi Burt, you’re right! I hadn’t stared that. So it simplifies the usage of transactions!
Thank you very much for the correction!