Desde que comenzamos en mi empresa con Grails, uno de los puntos donde estábamos un tanto inseguros era el tema de la forma en que se manejan las relaciones con GORM. Por una parte, nunca habíamos usado Hibernate ni ningun otro ORM, y por otro, siempre habíamos diseñado la base de datos y las relaciones entre las entidades de forma manual.
En estos dias de vacaciones estoy leyendo el (muy recomendable) libro "The Definitive Guide to Grails 2", con el que estoy afianzando algunos conceptos que ya he manejado en el ultimo año y estoy aprendiendo algunas otras técnicas que estoy seguro de que me me van a ser muy utiles en el corto plazo.
En particular, el capítulo 3 dedicado a las clases de dominio tiene un apartado sobre las relaciones entre las clases de dominio y el
mapeo que se realiza en base de datos que me ha resultado especialmente claro, asi que voy a intentar resumirlo y traducirlo por aqui por si a alguien mas le puede ser de utilidad.
Partamos de las dos siguiente clases simples:
class Car {
Engine engine
}
class Engine {
Car car
}
Claramente tenemos una relación
one-to-one simple: un coche tiene un motor y un motor tiene un coche. Ninguna de las dos clases es
propietaria de la relación. Y probablemente esto no sea lo que necesitemos en nuestra aplicacion.
Si queremos (lo mas probable en el caso anterior) definir cual es la clase propietaria de la relación, deberíamos hacerlo de la siguiente forma, definiendo explicitamente que un motor
pertenece a un coche:
class Car {
Engine engine
}
class Engine {
static belongsTo = [car:Car]
}
De esta forma decimos a Grails que el coche es la
parte propietaria de la relación, que el motor
pertenece al coche, y no al revés.
El parámetro [car:Car] de la propiedad belongsTo es un mapa (y podríamos incluir varios valores: [car:Car, otherClass:TheOtherClass, ....]). La clave del mapa (car) representa el nombre de la propiedad que se añadirá (y usaremos luego para recuperar datos) a la clase Engine. Con esta definición, conseguiremos tambien que cuando se realice el
mapeo en base de datos, tengamos una
foreign-key en la tabla CAR que referencia a la clave primaria (el ID) de la tabla ENGINE
Obviamente, en algunos casos puede ser deseable tambien disponer de la
foreign-key en el otro sentido, esto es: tener la
foreign-key en la tabla ENGINE referenciando a la clave primaria de CAR. La forma de hacerlo en definiendo la propiedad
hasOne en la clase Car, de la siguiente forma:
class Car {
static hasOne = [engine: Engine]
}
En algunas circunstancias podemos tener situaciones donde la relacion necesita un lado propietario de la misma, pero no se necesita tener la referencia (back-reference: el ID del propietario en la clase
debil de la relación). Grails soporta este tipo de relación de forma similar al caso anterior, pero indicanto solamente el nombre de la clase en lugar de usar una mapa:
class Engine {
static belongsTo = Car
}
Una de las aplicaciones mas claras de esta aproximación es que automatizan los borrados en cascada: cada vez que se borra un coche de la base de datos se borrará el motor asociado.
Las relaciones de uno-a-muchos se representan facilmente tambien en Grails. Supongamos que tenemos una aplicacion con Albums de música que tienen canciones (Song) y que pertenecen a un Artista. La definicion de estas tres clases podría ser algo asi:
class Artist {
String name
static hasMany = [albums:Album]
}
class Album {
String title
static hasMany = [songs:Song]
static belongsTo = [artist:Artist]
}
class Song {
String title
Integer duration
static belongsTo = Album
}
En el ejemplo anterior, un Artist tiene muchos Albums, y un Album perteneca a su Artist propietario. De la misma forma, un Album tiene muchas canciones, y cada cancion tiene su Album propietario. En cualquier caso, una canción no tiene referencia a su Album: dada una canción no podremos saber a que Album pertence, aunque desde los Albumes si que podremos obtener su lista de canciones.
Como vemos, la clase Artist tiene una propiedad, albums, que es una coleccion de objetos Album. El tipo de coleccion que Grails usa por defecto en estos casos es un java.util.Set , que es una coleccion sin orden. Si necesitas otro tipo de coleccion (por ejemplo: List o SortedSet que mantienen un orden, lo que puede penalizar el rendimiento) puedes hacerlo pero tendrás que declararlo explícitamente, por ejemplo:
class Album {
String title
static hasMany = [songs:Song]
static belongsTo = [artist:Artist]
SortedSet songs
}
Nota: para que este comportamiento funcione, la clase Song tiene que implementar el interface
Comparable
No voy a entrar en la problemática del rendimiento que puede provocar el uso de estas propiedades, pero si que recomiendo echar un vistazo a estas dos entradas (
1 y
2) de Burt Beckwith sobre las recomendaciones de usar el concepto de
Bag de Hibernate para estas relaciones u otro tipo de técnicas.