lunes, 22 de octubre de 2012

Trampas y trucos de GORM - Parte 1


¿Eres nuevo en Grails?. O ¿has tenido tu primera pesadilla con GORM?. Si es asi, tal vez te interese leer estas anotaciones de Peter Ledbrook traducidas.  En ellas se intentará explicar no solo las peculiaridades que a veces nos hacen tener errores sino tambien porqué GORM se comporta de esa forma.

Supongo que al menos ya sabrás que GORM es la libreria de acceso a base de datos que se utiliza con Grails. Está basada en el que probablemente es el ORM más popular, Hibernate. Como ya puedes suponer, Hibernate es una herramiente muy potente y flexible, pero usarla tiene su coste, y muchos de los problemas que suelen tener los usuarios de GORM tienen su origen en la forma en que funciona Hibernate. GORM intenta ocultar los detalles de la implementación de la mejor forma posible, pero a veces hay cosas que no funcionan tan bien como uno desearía.

En esta anotación describiremos las bases de persistencia de objetos en una base de datos. Suena sencillo, pero incluso en cosas tan aparentemente simples GORM tal vez no funcione de la forma en que tu lo esperas.

Cuando llamo a save(), quiero decir save!: guárdalo!


Los problemas guardando instancias de clases de dominio son con seguridad los primeros con los que se encontrará un desarrollador. Puede suceder que te encuentres en el caso de "lo he guardado, asi que ¿porqué no está en la base de datos?". Si te sirve de consuelo, a todos nos ha pasado. Pero ¿por qué sucede?. Puede haber un par de posibilidades.

No te olvides de la validación!

Cada vez que llamas al método save() para guardar una instacia de una clase de dominio, Grails la valida usando las constraints que has definido. Si algun valor viola estas constraints, grails no guardará en registro en la base de datos y se adjuntarán los errores correspondientes en tu instancia. El problema es que este proceso es silencioso: no te darás cuenta de lo que ha pasado a no ser que verifiques el valor de vuelta de save() o hagas una llamada al método hasErrors().

Cuando estas asociando tu instancia de la clase de dominio con los datos que ha introducido un usuario por pantalla, normalmente este es el comportamiento deseado: no es una sorpresa que los usuarios se equivoquen al introducir los datos o que estos no sigan el comportamiento que has definido, asi que hacer saltar una Exception en un caso asi, simplemente no parece apropiado. Es mejor verificar lo que ha pasado en la llamada al método save() para informar al usuario y que pueda actuar en consecuencia. El método save() devolverá null si ha habido algun problema, o la instancia de la clase en caso de que todo haya salido bien.

 def book = new Book(params)  
 if (!book.save()) {  
   // Fallo! Mostrar errores al usuario  
   ...  
 }  

Por otro lado, cuando estas introduciendo datos de prueba en la clase de BootStrap o en la consola Grails, normalmente esperaras que los datos sean correctos (¡los estas metiendo tu!), asi que si hay algún fallo de validación es porque has metido la pata. En este caso, seguramente preferirías no complicarte verificando el valor de cada llamada a save() y que si tienes un error Grails te avise con una Exception. No es el comportamiento por defecto, pero puedes usarlo de forma sencilla con el argumento failOnError de save():



book.save(failOnError: true)


Si insistes, incluso puedes hacer que este sea el comportamiento por defecto: simplemente asigna la propiedad grails.gorm.failOnError a true en grails-app/conf/Config.groovy. Y por cierto, no te olvides de que todas las propiedades de una clase de dominio tienen por defecto una constraint de nullable: false!

La sesión de Hibernate


En alguna ocasión extraña, te encontrarás el caso de salvar una instancia de tu clase sólo para descubrir que la siguiente consulta a base de datos no la encuentra, incluso aunque haya pasado todas las validaciones.

Esto puede ser a causa del funcionamiento de Hibernate por debajo, ya que Hibernate es un ORM basado en sesiones. Este es un punto importante y para sentirte cómodo con GORM deberías comprender perfectamente el impacto que esto puede tener en tus aplicaciones.

¿Que es una sesion?. Basicamente es un cache en memoria de objetos que van y vienen de la base de datos. Cuando guardas una instancia de una clase de dominio, está implícitamente unida a la sesión: esto es: se añado al caché y se convierte en un objeto manejado por hibernate. Pero se persiste a la base de datos en este punto! . El siguiente diagrama muestra este comportamiento:





Cuando guardas una instancia, está inmediatamente disponible para esa sesión, pero Hibernate puede decidir que prefiere persistir a base de datos mas tarde para optimizar el acceso a la base de datos. Normalmente no te darás cuenta de estas cosas porque Hibernate y Grails se encargan de ello, pero a veces sucederán estas cosas.

Como puedes imaginar, Grails te permite un cierto grado de control sobre estos casos. ¿Has visto  código como este en algun ejemplo? :



book.save(flush: true)


Ese flush: true fuerza a Hibernate a persistir los cambios en la base de datos ahora mismo. Es lo que se llama hacer flush de la sesion.

Ahora. claro, estarás pensando que porqué no poner flush: true por todo todos lados. No lo hagas. Deja que Hibernate haga su trabajo y solo usa este recurso cuando necesites hacerlo, por ejemplo al final de un proceso de actualizacion batch o por lotes. Sólo deberias usarlo cuando no veas datos en la base de datos que deberían estar ahí. Ya se que parece un poco sistema de ensayo y error,  pero el como funcione en tu caso dependerá de la implementacion de tu base de datos y de otros factores. Un caso donde probablemente sea muy util forzar la escritura a disco es cuando este interactuando con otra aplicación que acceda a la misma base de datos.

Y ahora que no quiero que guardes, ¿lo haces?


En el apartado anterior hemos visto que a veces una llamada al método save() no guarda los datos, al menos no instantaneamente. Pero considera ahora el caso inverso: objetos que se persisten en la base de datos sin su llamada a save() correspondiente. Si no te ha pasado nunca, te garantizo que antes o despues te pasará. ¿Porqué sucede?

Hibernate tiene el concepto de dirty-checking (N.del.T: podriamos traducirlo como verificación de datos que han cambiado desde que se leyeron o algo asi. Mejor lo dejo en inglés).  Este comportamiento verifica que datos salieron de la base de datos, y si han cambiado en memoria, intenta escribirlos de vuelta. Como puede parecer -y lo es- un poco lioso, mejor lo vemos con un ejemplo: Supongamos la clase de domino Libro con un par de propiedades titulo y autor y el siguiente código en su clase controller:



def b = Book.findByAuthor(params.author)
b.title = b.title.reverse()


Fíjate en que no hay ninguna llamada a save(), pero cuando la petición haya finalizado te encontrarás con que el titulo está al reves en la base de datos. El cambio se ha persistido en la base de datos sin una llamada explicita a save(). Esto ha sucedido porque:

  1. El libro esta unido a la sesion (por haber sido recuperado por una query)
  2. La propiedad titulo es persistente (todas las propiedades lo son a no ser que las configures como transient)
  3. El valor de la propiedad ha cambiado cuando la sesion se cierra
Hemos mencionado que los objetos estan unidos a la sesion, pero ¿como se unen?.  Si usas GORM para recuperar cualquier objeto, por ejemplo con el método get() o cualquier tipo de query, los objetos quedan automáticamente unidos a la sesion. Si creas un objeto usando new(), el objeto no está unido a la sesion hasta que no llames al método save()

Segundo: las clases de dominio son persistentes por defecto, asi que tienen una columna en la base de datos relacionada con cada propiedad que hayas definido para guardar sus valores. Pero puedes hacer que la propiedades sean transitorias añadiendolas la lista estatica transients , que se usa precisamente para no guardar esos valores en la base de datos. 

Para terminar, hemos mencionado que los cambios se persisten si existe cuando se cierra la sesion. ¿Que quiere decir esto?. Que para hacer cualquier operacion con Hibernate tienes que tener abierta una sesion. Una vez que se cierra la sesion no puedes usarla para acceder a la base de datos. La sesion es flushed (forzada a escribirse en base de datos) al final de la accion de nuestro controlador (Grails automaticamente abre una sesion al principio de la peticion y la cierra al final)

¿Podemos evitar este comportamiento?. Claro que si. Una opcion es llamar nosotros a save() en nuestra instancia de clase de dominio: si alguna de las propiedades falla las validaciones, los cambios no persistirán.  Por supuesto, aunque los datos cumplan con las validaciones puede que no querramos que persistan, y podemos llamar al método discard() en nuestra instancia de clase de dominio. Esta operación no restaura los valores originales, pero asegura que los nuevos no se guardan en la base de datos.

Hasta aqui tenemos una buena cantidad de información para ir digiriendo hasta la próxima entrada. La clave es comprender como la sesion de Hibernate puede afectar al comportamiento de tus clases de dominio. En las próximas anotaciones habrá mas información y ejemplos.

En general, se recomienda que siempre utilices save() para guardar tus objetos en lugar de fiarte del dirty-checking. Hace tu código mas claro tiene la ventaja de que tus cambios son primero validados y luego guardados. Tambien es muy recomendable verificar el valor de vuelta del método save() y utilizar el parámetro failOnError: true cuando estés dando de alta datos de prueba o en el bootstrap.

Si lo que has leido hasta aqui de GORM te ha intimidado, recupérate, no es para tanto. Hace que jugar con la base de datos se mas sencillo y divertido, y confío en que esta serie de artículos te sean de ayuda para solucionar los problemas que puedas ir encontrando.



Anotación original, de Peter Ledbrook: Gorm Gotchas - Part 1

Anotaciones relacionadas: Trampas y Trucos de GORM - Parte 0

Ultimos enlaces compartidos

Aerotrastornados - Blog Aeronáutico