-
oct13
Referenciar clases del dominio desde un script Gant (inglés)
publicado por Enrique Medina Montenegro en Grails
262 visitas -
oct13
Clases del dominio GORM dinámicas (inglés)
publicado por Enrique Medina Montenegro en Grails
320 visitas -
mar31
Migración de 1.1.1 a 1.2.x con clases de dominio anotadas (inglés)
publicado por Enrique Medina Montenegro en Build
309 visitas
Cuidado con los mapas y la caché de 2º nivel en las clases del dominio
publicado por Enrique Medina Montenegro
Enrique Medina Montenegro
Con más de 14 años de experiencia en el mundo de las TI, donde comenzó desarrollando aplicaciones de escritorio en Delphi o Visual Basic, este Ingeniero en Informática por la Universidad de Alicante (1991-1996) ha ido perfilando su actividad profesional hacia las arquitecturas J2EE, donde siempre ha seguido muy atento, e incluso colaborado en ocasiones, con proyectos open-source como MyFaces, Spring, Hibernate, Groovy o Grails. Ocupando puestos desde Programador Junior hasta Arquitecto Senior de Soluciones, Enrique ha sido testigo de cómo ha ido evolucionando la tecnología en torno al desarrollo de aplicaciones web, adquiriendo un conocimiento y experiencia que le permiten evaluar con detalle las necesidades de cada proyecto y aplicar las herramientas que maximizan su productividad. Actualmente, Enrique se ha especializado en el framework de desarrollo Grails, y ejerce la Dirección Técnica de proyectos basados, entre otras, en esta tecnología.
En el día a día, nuestro equipo de desarrollo Grails se encuentra con algunas incidencias que inicialmente no tienen explicación, pero que una vez se analizan y depuran, se convierten en una lección aprendida para futuros proyectos. Y como nuestra filosofía es compartir el conocimiento con todos nuestros amigos del Observatorio, pues aquí os dejamos un consejo que creemos os puede ahorrar muchos quebraderos de cabeza y horas perdidas (Grails 1.2.1 con plugin Hibernate 1.2.1).
El caso es que en una de nuestras clases del dominio habíamos definido un mapa para poder almacenar valores por clave; un ejemplo típico es una clase que representa un usuario, y queremos almacenar su “nick” en función del programa de chat que utiliza:
class Usuario implements Serializable {
String login
String passwd
String nombre
String apellidos
// Diferentes 'nicks' por tipo de chat.
// ['MSN':'emedina_msn', 'GTalk':'e.medina.m', 'Yahoo':'emedinam', ...]
Map nicks
}
Cuando definimos un campo de tipo Map en nuestra clase del dominio sin especificar ningún tipo de configuración adicional, Grails genera una tabla auxiliar para almacenar los distintos pares ‘clave:valor’ del mapa, como se observa al generar el esquema de BD:
create table usuario (id bigint generated by default as identity (start with 1), version bigint not null, nombre varchar(255) not null, passwd varchar(255) not null, apellidos varchar(255) not null, login varchar(255) not null, primary key (id)); create table usuario_nicks (nicks bigint, nicks_idx varchar(255), nicks_elt varchar(255) not null);
Además, por defecto, Grails siempre indica como tipo para las columnas que almacenarán la clave y el valor (_idx y _elt, respectivamente) un VARCHAR(255), que puede que no sea lo que estamos buscando. En tal caso, es posible indicar el tipo y el tamaño usando algo de configuración adicional en la clase anterior:
class Usuario implements Serializable {
String login
String passwd
String nombre
String apellidos
// Diferentes 'nicks' por tipo de chat.
// ['MSN':'emedina_msn', 'GTalk':'e.medina.m', 'Yahoo':'emedinam', ...]
Map nicks
// Redundante, pero para que se vea el efecto.
static hasMany = [nicks: String]
static mapping = {
nicks indexColumn:[length:50], length:100
}
}
Lo que siempre decimos en nuestros artículos: Grails hace por ti, pero si quieres cambiarlo, es posible y sencillo. Una vez tenemos nuestra clase ‘Usuario’ creada y configurada como nos interesa, vamos a explicar el asunto principal de este artículo, que es advertiros sobre el uso de la ‘caché’ para este tipo de campos.
Si queremos activar la caché de segundo nivel para optimizar el acceso a datos, lo primero que intentamos es cachear las instancias de ‘Usuario’. Para ello, únicamente debemos añadir lo siguiente a la sección ‘mapping’ de la clase:
class Usuario implements Serializable {
String login
String passwd
String nombre
String apellidos
// Diferentes 'nicks' por tipo de chat.
// ['MSN':'emedina_msn', 'GTalk':'e.medina.m', 'Yahoo':'emedinam', ...]
Map nicks
// Redundante, pero para que se vea el efecto.
static hasMany = [nicks: String]
static mapping = {
nicks indexColumn:[length:50], length:100
cache true
}
}
Ahora, al pedir por primera vez un determinado usuario, se ejecutarán 2 consultas SQL contra la BD: una para obtener el usuario, y otra para obtener los distintos nicks de ese usuario, ya que dichos nicks se encuentran en una tabla separada de la de usuario en BD, como se ha explicado más arriba; sin embargo, en subsecuentes peticiones del mismo usuario, nos ahorraremos una de las consultas SQL, porque Hibernate nos devolverá la instancia cacheada a nivel de JVM del usuario (recordad que estamos hablando de caché de segundo nivel, es decir, a nivel de JVM), pero todavía seguirá ejecutando una consulta SQL para obtener los nicks. Vamos a intentar optimizarlo más aún:
class Usuario implements Serializable {
String login
String passwd
String nombre
String apellidos
// Diferentes 'nicks' por tipo de chat.
// ['MSN':'emedina_msn', 'GTalk':'e.medina.m', 'Yahoo':'emedinam', ...]
Map nicks
// Redundante, pero para que se vea el efecto.
static hasMany = [nicks: String]
static mapping = {
nicks indexColumn:[length:50], length:100, fetch:'join', cache:true
cache true
}
}
¿Véis la diferencia? Hemos complementado la variable ‘nicks’ de la sección ‘mappings’ con el modo de ‘fetching’ (cómo debe traerse la asociación con ‘nicks’, si a través de varias ‘select’ adicionales o mediante un ‘outer join’ en una única ‘select’) e indicando que queremos que se cachee el mapa de valores ‘nicks’. Si ahora volvemos a realizar el experimento de pedir el usuario, Hibernate sólo generará una consulta SQL para obtener tanto el usuario como sus nicks, y para ello utilizará un ‘outer join’. Además, en teoría, una vez obtenidos los ‘nicks’ del usuario, los cacheará junto con el propio usuario. Pero, si ya estamos cacheando al propio usuario, y además le decimos que cachee al mapa ‘nicks’ indicando el modo ‘fetch’, se produce un comportamiento curioso que no es el esperado: Hibernate sólo cachea el primero de los ‘nicks’ disponibles, y no el resto, de forma que cuando más adelante pedimos un ‘nick’ distinto del que se ha cacheado, pues nos devuelve un ‘null’ y tenemos un problema.
Aunque parece algo enreversado, la explicación es que a Hibernate no le gusta que se le indique para un mapa el modo ‘fetch’ y que además se cachee. Sin embargo, si quitamos el modo ‘fetch’:
class Usuario implements Serializable {
String login
String passwd
String nombre
String apellidos
// Diferentes 'nicks' por tipo de chat.
// ['MSN':'emedina_msn', 'GTalk':'e.medina.m', 'Yahoo':'emedinam', ...]
Map nicks
// Redundante, pero para que se vea el efecto.
static hasMany = [nicks: String]
static mapping = {
nicks indexColumn:[length:50], length:100, cache:true
cache true
}
}
Ahora todo funciona a la perfección: en la primera petición de un usuario determinado, se ejecutan 2 consultas SQL, pero a partir de ese momento, y mientras el usuario esté en nuestra caché, no se volverá a molestar a la BD para nada, con la consecuente mejora en rendimiento asociada. Interesante, ¿verdad?
Y esto es todo amigos. En futuros artículos seguiremos profundizando en las tripas de GORM para poder sacarle todo el jugo que podamos. Como dicen mis amigos americanos, ¡stay tuned!
Y no os olvidéis de dejarnos vuestros comentarios si alguno de nuestros artículos os ha sido de utilidad, o si encontráis algo incorrecto o que no podéis hacer funcionar desde vuestro lado. Estaremos encantados de echaros una mano.


Entradas relacionadas 
Comentarios recientes
Buenas,
muy explicativo el artículo! Trabajo con Hibernate cada día (no unido con Grails como sería mi gusto) y optimizar las consultas, accesos, rendimiento es algo que me interesa mucho. Hace poco tuvimos un problema de redundancia de objetos por la caché de primer nivel y son de esas cosas que, pasan a veces y que, cuando te pones a investigar, aprendes qué y porqué. Intentaré sacar hueco para mirar estos temas y algunas ideas me han surgido a partir de este artículo. Gracias!
Genial también Observatorio de Grails
Felicidades!
Un saludo
Encantados de poder ayudar. Y encantados de ver que nuestros artículos llegan a la gente y les son de utilidad.
Buenas, genial artículo, como siempre!
El tema de la chacé de segundo nivel es muy interseante, estoy en desarrollo de un proyecto y todavía no lo he utilizado, primero quiero hacer funcionar basicamente toda la aplicación para después pasar a optimizar estas pequeñas cosas, así que, a mis favoritos va el artículo.
Quería haceros una pregunta, en vuestros desarrollos, cuando hay que clasificar ciertos datos, por eemplo un TipoActividad que se relacionaria con una Actividad, como lo soleis hacer? con un inList:['Actividad de benivenida','actividad x'].
O directamente, creais la clase TipoActividad aunque sea solo con un String para el nombre y hacer la relación con Actividad por id?
De esta seguna manera a los usuarios les dá mas libertad, pues sin tener al informático delante pueden añadir categorías etc.
Pero el problema que estoy teniendo es que el número de clases de dominio aumenta demasiado en mi opinión.
Como lo solucionais vosotros?
Creando un tipo List?
Gracias
josem,
En dichos casos siempre solemos optar por un tipo enumerado, que además Hibernate mapea de forma transparente para Grails, por lo que ni nos damos cuenta de que lo estamos usando
enum class TipoActividad {
BIENVENIDA, OTRA
}
habría que crear una clase no? no la puedo poner en otra clase de dominio, verdad?
Puedes hacerlo como quieras, pero normalmente nosotros creamos un fichero groovy exclusivo para el tipo enumerado… Pero supongo que es cuestión de gustos
TipoEvento.groovy
public enum TipoEvento {
BIENVENIDA, CHARLA, DESPEDIDA
}
Evento.groovy
class Evento {
String nombre
TipoEvento tipoEvento
static constraints = {
}
}
..
def e = new Evento(nombre:’Presentación Grails’,tipoEvento:TipoEvento.BIENVENIDA).save()
así es como me decías, mas o menos?
¡Exacto!
Qué agustito, llevarme esas clases de enum a otra parte, por ejemplo a sources groovy/enums y no tener el domain classess petado
josem,
Lleva cuidado con el uso de palabras reservadas como nombres de paquetes…
(no estoy 100% seguro de si ‘enums’ lo es, pero pon ‘enumerados’ y te ahorras disgustos)