¿Clave ajena u otra tabla adicional en relaciones 1:M? Cómo usar una librería de etiquetas desde un servicio en Grails
mar 05

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 artículos anteriores sobre GORM hemos visto algunas de las “perlas” que nos ofrece sin coste alguno, así como la forma de influir en la generación del esquema de BD. Pero como ya esperábamos, las posibilidades de Grails van mucho más allá. En este sentido, vamos a echarle un vistazo a la configuración avanzada de GORM cuando una misma clase de nuestro dominio define tanto relaciones 1:M como M:M.

Y como una imagen vale más que mil palabras, vamos a usar un ejemplo sencillo, pero muy completo y potente, basado en una categorización de elementos. El siguiente diagrama UML muestra nuestro dominio de actuación:

Diagrama UML para categorización de elementos a tres niveles

Como se deduce del diagrama UML, nuestra aplicación nos va a permitir categorizar en tres niveles mediante una jerarquía estricta, esto es, donde cada nivel de la jerarquía sólo tiene un padre, una serie de elementos en general. Sin embargo, el propósito de este artículo no es discutir sobre la modelización conceptual de una categorización, sino la forma en que Grails nos permite mapear el caso específico que aquí se presenta con el uso de GORM y, principalmente, dos funcionalidades clave: belongsTo y hasMany.

Puesto el dominio de nuestro ejemplo en contexto, ¿qué tiene de particular este modelo? Aparentemente se trata de una modelización típica de esquemas relacionales, donde tenemos relaciones 1:M y M:N, como casi siempre. ¿Entonces, qué es lo que hace tan especial a este ejemplo? No os preocupéis: la forma en que debemos empezar a definir las relaciones usando GORM en Grails nos irá abriendo los ojos poco a poco. Comencemos, pues, con las relaciones de jerarquía entre los tres niveles de categorías:

class Categoria1 {

	String nombre
	String descripcion

	Date dateCreated
	Date lastUpdated

	// Una categoría de nivel 1 sólo puede tener categorías hija de nivel 2.
	static hasMany = [categorias2: Categoria2]

}

class Categoria2 {

	String nombre
	String descripcion

	Date dateCreated
	Date lastUpdated

	// Una categoría de nivel 2 debe tener sólo una categoría padre de nivel 1.
	static belongsTo = [categoria1: Categoria1]

	// Una categoría de nivel 2 sólo puede tener categorías hija de nivel 3.
	static hasMany = [categorias3: Categoria3]

}

class Categoria3 {

	String nombre
	String descripcion

	Date dateCreated
	Date lastUpdated

	// Una categoría de nivel 3 sólo puede tener una categoría padre de nivel 2.
	static belongsTo = [categoria2: Categoria2]

}

Hasta aquí todo correcto, ¿verdad? Simplemente hemos utilizado los mecanismos belongsTo y hasMany de forma estándar tal y como se especifica en la documentación de GORM para Grails. Si generamos el esquema de BD, vemos que efectivamente se crean 3 tablas con sus correspondientes claves ajenas:

alter table categoria2 drop constraint FK4D47455FBCFBE91A;
alter table categoria3 drop constraint FK4D474560BCFC5D7A;
drop table categoria1 if exists;
drop table categoria2 if exists;
drop table categoria3 if exists;
create table categoria1 (id bigint generated by default as identity (start with 1), version bigint not null, nombre varchar(255) not null, descripcion varchar(255), last_updated timestamp, date_created timestamp not null, primary key (id));
create table categoria2 (id bigint generated by default as identity (start with 1), version bigint not null, nombre varchar(255) not null, descripcion varchar(255), last_updated timestamp, date_created timestamp not null, categoria1_id bigint not null, primary key (id));
create table categoria3 (id bigint generated by default as identity (start with 1), version bigint not null, nombre varchar(255) not null, descripcion varchar(255), last_updated timestamp, date_created timestamp not null, categoria2_id bigint not null, primary key (id));
alter table categoria2 add constraint FK4D47455FBCFBE91A foreign key (categoria1_id) references categoria1;
alter table categoria3 add constraint FK4D474560BCFC5D7A foreign key (categoria2_id) references categoria2;

Vamos, ahora, a por las relaciones M:N definidas entre cada una de las categorías y los elementos. No lo he comentado antes, pero la idea detrás de esta modelización es soportar la categorización en distintos niveles de cualquier elemento; por ejemplo, un elemento A puede categorizarse como de nivel 2, pero también como nivel 3, e incluso pertenecer a varias categorías distintas dentro del mismo nivel. Flexible, ¿eh? Continuemos, entonces, con nuestra definición en Grails; primero, la nueva clase del dominio Elemento:

class Elemento {

	String nombre
	String descripcion

	// Un elemento puede categorizarse tantas veces como se quiera en cualquier nivel.
	static hasMany = [categorias1: Categoria1, categorias2: Categoria2, categorias3: Categoria3]

}

No nos olvidemos de que se trata de relaciones bidireccionales, por lo que añadimos debemos añadir también la relación en las clases de categorías:

class Categoria1 {

	String nombre
	String descripcion

	Date dateCreated
	Date lastUpdated

	// Una categoría de nivel 1 puede contener muchos elementos.
	static hasMany = [categorias2: Categoria2, elementos: Elemento]
}

class Categoria2 {

	String nombre
	String descripcion

	Date dateCreated
	Date lastUpdated

	// Una categoría de nivel 2 debe tener sólo una categoría padre de nivel 1.
	static belongsTo = [categoria1: Categoria1]

	// Una categoría de nivel 2 puede contener muchos elementos.
	static hasMany = [categorias3: Categoria3, elementos: Elemento]
}

class Categoria3 {

	String nombre
	String descripcion

	Date dateCreated
	Date lastUpdated

	// Una categoría de nivel 3 sólo puede tener una categoría padre de nivel 2.
	static belongsTo = [categoria2: Categoria2]

	// Una categoría de nivel 3 puede contener muchos elementos.
	static hasMany = [elementos: Elemento]
}

Si ahora le pedimos a Grails que amablemente nos exporte el esquema de BD, nos llevamos una decepción:

Error loading plugin manager: No owner defined between domain classes [class Categoria1] and [class Elemento] in a many-to-many relationship. Example: def belongsTo = Elemento
	at _PluginDependencies_groovy$_run_closure8_closure70.doCall(_PluginDependencies_groovy:616)
	at _PluginDependencies_groovy$_run_closure8_closure70.doCall(_PluginDependencies_groovy)
	at _GrailsSettings_groovy$_run_closure10.doCall(_GrailsSettings_groovy:287)
	at _GrailsSettings_groovy$_run_closure10.call(_GrailsSettings_groovy)
	at _PluginDependencies_groovy$_run_closure8.doCall(_PluginDependencies_groovy:596)
	at _GrailsBootstrap_groovy$_run_closure1.doCall(_GrailsBootstrap_groovy:69)
	at SchemaExport$_run_closure4.doCall(SchemaExport.groovy:99)
	at gant.Gant$_dispatch_closure4.doCall(Gant.groovy:324)
	at gant.Gant$_dispatch_closure6.doCall(Gant.groovy:334)
	at gant.Gant$_dispatch_closure6.doCall(Gant.groovy)
	at gant.Gant.withBuildListeners(Gant.groovy:344)
	at gant.Gant.this$2$withBuildListeners(Gant.groovy)
	at gant.Gant$this$2$withBuildListeners.callCurrent(Unknown Source)
	at gant.Gant.dispatch(Gant.groovy:334)
	at gant.Gant.this$2$dispatch(Gant.groovy)
	at gant.Gant.invokeMethod(Gant.groovy)
	at gant.Gant.processTargets(Gant.groovy:495)
	at gant.Gant.processTargets(Gant.groovy:480)

Pero, ¿qué ha pasado aquí? Bien, que no cunda el pánico, esto es normal. Lo único que ocurre es que GORM se queja abiertamente de que no existe un “propietario (owner)” en esta relación bidireccional, porque obviamente no lo hemos definido (GORM es listo, pero no tanto). Por lo tanto, ¿qué significa eso de “propietario en una relación bidireccional? Pues es muy sencillo, GORM necesita que le indiquemos qué clase (qué lado de la relación) toma la responsabilidad de persistir la propia relación, esto es, aplicar los cambios en cascada cuando se crean, modifican o eliminan nuevos objetos relacionados entre sí. Todo este trabalenguas significa que podemos decidir si queremos crear categorías a partir de elementos, o al revés, elementos a partir de categorías. O todo lo contrario, porque, ¿qué ocurre si no queremos aplicar cambios en cascada en absoluto? Digamos que no queremos que al eliminar una categoría, automáticamente se eliminen todos los elementos que contiene, pero por supuesto tampoco nos interesa que al eliminar un elemento se eliminen las categorías a las que pertenece. Vaya lío íbamos a montar, ¿no?

Centrémonos entonces en el caso de uso donde no queremos aplicar cambios en cascada. ¿Qué necesitamos configurar en nuestras clases? Lo primero es decidir qué lado de la relación será el propietario, y entonces deshabilitar los cambios en cascada para esa clase. Tomemos para nuestro ejemplo, que el propietario es la clase Elemento:

class Elemento {

	String nombre
	String descripcion

	// Un elemento puede categorizarse tantas veces como se quiera en cualquier nivel.
	static hasMany = [categorias1: Categoria1, categorias2: Categoria2, categorias3: Categoria3]

	static mapping = {
		categorias1 cascade: 'none'
		categorias2 cascade: 'none'
		categorias3 cascade: 'none'
	}

}

Volvemos de nuevo a exportar el esquema de BD, y se nos cae de nuevo el alma al suelo: el mismo error que antes. Pero, ¿qué estamos haciendo mal? ¿Qué se nos ha olvidado aquí? Un momento, hemos indicado que no queremos cambios en cascada (el código se ve perfectamente en el ejemplo de arriba), y hemos decidido que el propietario sea la clase Elemento, pero realmente, ¿se lo hemos dicho a GORM claro y alto para que lo entienda? Me temo que no… porque para ello debemos utilizar el belongsTo en las clases que forman parte del otro lado de la relación, esto es, en las clases de categorías. Identificado y analizado el problema, nos disponemos a modificar las clases de categorías para indicarle a GORM en cada una de ellas que la clase Elemento es el propietario de cada una de las relaciones. Para tal fin, sólo debemos añadir la siguiente configuración:

	static belongsTo = Elemento

Pero, un momento, esta forma de especificar el belongsTo es distinta de la forma en que ya lo estamos usando en nuestras clases de categorías para definir las otras relaciones 1:M entre ellas. Entonces, ¿qué hago ahora? Quizás, lo primero que intentaríamos sería este código (se muestra sólo la clase Categoria2 porque es la que contiene ambos tipos de relaciones):

class Categoria2 {

	String nombre
	String descripcion

	Date dateCreated
	Date lastUpdated

	// El propietario de mi relación M:N es el Elemento.
	static belongsTo = [categoria1: Categoria1, Elemento]

	// Una categoría de nivel 2 puede contener muchos elementos.
	static hasMany = [categorias3: Categoria3, elementos: Elemento]
}

Ejecutamos la aplicación para comprobar si nos ha dado resultado este cambio en la clase, y de nuevo un error, esta vez de compilación:

Running script /Users/emedina/Documents/Projects/Grails/grails-1.2.1/scripts/RunApp.groovy
Environment set to development
  [groovyc] Compiling 1 source file to /Users/emedina/Documents/Projects/Categorias/target/classes
  [groovyc] org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed, /Users/emedina/Documents/Projects/Categorias/grails-app/domain/Categoria2.groovy: 18: Unexpected node type: EXPR found when expecting type: LABELED_ARG at line: 18 column: 46. File: /Users/emedina/Documents/Projects/Categorias/grails-app/domain/Categoria2.groovy @ line 18, column 46.
  [groovyc]    To = [categoria1: Categoria1, Elemento]
  [groovyc]                                  ^
  [groovyc]
  [groovyc] 1 error
Compilation error: Compilation Failed

No hace falta ser un experto en Grails para darse cuenta del problema: la definición del belongsTo es un mapa, y por lo tanto no podemos añadir un elemento sin indicar un par “clave:valor“. Correcto, tienes razón Grails, lo cambio (pero no olvidemos que hay que hacerlo en todas las clases):

class Categoria1 {

	String nombre
	String descripcion

	Date dateCreated
	Date lastUpdated

	// El propietario de mi relación M:N es el Elemento.
	static belongsTo = Elemento

	// Una categoría de nivel 1 puede contener muchos elementos.
	static hasMany = [categorias2: Categoria2, elementos: Elemento]
}

class Categoria2 {

	String nombre
	String descripcion

	Date dateCreated
	Date lastUpdated

	// El propietario de mi relación M:N es el Elemento.
	static belongsTo = [categoria1: Categoria1, elemento: Elemento]

	// Una categoría de nivel 2 puede contener muchos elementos.
	static hasMany = [categorias3: Categoria3, elementos: Elemento]
}

class Categoria3 {

	String nombre
	String descripcion

	Date dateCreated
	Date lastUpdated

	// El propietario de mi relación M:N es el Elemento.
	static belongsTo = [categoria2: Categoria2, elemento: Elemento]

	// Una categoría de nivel 3 puede contener muchos elementos.
	static hasMany = [elementos: Elemento]
}

Otra vez le damos al “grails run-app” confiados y… ¡oh, no, no puede ser, otra vez el error del propietario!:

Running Grails application..
2010-03-05 11:33:57,083 [main] ERROR context.ContextLoader  - Context initialization failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'pluginManager' defined in ServletContext resource [/WEB-INF/applicationContext.xml]: Invocation of init method failed; nested exception is org.codehaus.groovy.grails.exceptions.GrailsDomainException: No owner defined between domain classes [class Elemento] and [class Categoria3] in a many-to-many relationship. Example: def belongsTo = Categoria3
	at org.grails.tomcat.TomcatServer.start(TomcatServer.groovy:135)
	at grails.web.container.EmbeddableServer$start.call(Unknown Source)
	at _GrailsRun_groovy$_run_closure5_closure12.doCall(_GrailsRun_groovy:158)
	at _GrailsRun_groovy$_run_closure5_closure12.doCall(_GrailsRun_groovy)
	at _GrailsSettings_groovy$_run_closure10.doCall(_GrailsSettings_groovy:287)
	at _GrailsSettings_groovy$_run_closure10.call(_GrailsSettings_groovy)
	at _GrailsRun_groovy$_run_closure5.doCall(_GrailsRun_groovy:149)
	at _GrailsRun_groovy$_run_closure5.call(_GrailsRun_groovy)
	at _GrailsRun_groovy.runInline(_GrailsRun_groovy:115)
	at _GrailsRun_groovy.this$4$runInline(_GrailsRun_groovy)
	at _GrailsRun_groovy$_run_closure1.doCall(_GrailsRun_groovy:59)
	at RunApp$_run_closure1.doCall(RunApp.groovy:33)
	at gant.Gant$_dispatch_closure4.doCall(Gant.groovy:324)
	at gant.Gant$_dispatch_closure6.doCall(Gant.groovy:334)
	at gant.Gant$_dispatch_closure6.doCall(Gant.groovy)
	at gant.Gant.withBuildListeners(Gant.groovy:344)
	at gant.Gant.this$2$withBuildListeners(Gant.groovy)
	at gant.Gant$this$2$withBuildListeners.callCurrent(Unknown Source)
	at gant.Gant.dispatch(Gant.groovy:334)
	at gant.Gant.this$2$dispatch(Gant.groovy)
	at gant.Gant.invokeMethod(Gant.groovy)
	at gant.Gant.processTargets(Gant.groovy:495)
	at gant.Gant.processTargets(Gant.groovy:480)
Caused by: org.codehaus.groovy.grails.exceptions.GrailsDomainException: No owner defined between domain classes [class Elemento] and [class Categoria3] in a many-to-many relationship. Example: def belongsTo = Categoria3
	... 23 more

¿Se entiende el problema? Parece ser que no podemos definir un elemento del mapa belongsTo si no es un par “clave:valor“, pero a la vez no podemos definir el propietario de la relación sin usar el belongsTo. ¿Qué hacemos entonces? ¿Cerramos el chiringuito y empezamos a despotricar sobre Grails? No, desde luego que no, pero esta respuesta ya os la imaginábais, ¿no? Echadle un vistazo a esta pequeña modificación del código en las clases de categorías:

class Categoria1 {

	String nombre
	String descripcion

	Date dateCreated
	Date lastUpdated

	// El propietario de mi relación M:N es el Elemento.
	static belongsTo = Elemento

	// Una categoría de nivel 1 puede contener muchos elementos.
	static hasMany = [categorias2: Categoria2, elementos: Elemento]
}

class Categoria2 {

	String nombre
	String descripcion

	Date dateCreated
	Date lastUpdated

	// Utilizamos una lista en vez de un mapa para definir las relaciones...
	static belongsTo = [Categoria1, Elemento]
	// ...y definimos el campo que hemos "perdido" de forma explícita.
	Categoria1 categoria1

	// Una categoría de nivel 2 puede contener muchos elementos.
	static hasMany = [categorias3: Categoria3, elementos: Elemento]
}

class Categoria3 {

	String nombre
	String descripcion

	Date dateCreated
	Date lastUpdated

	// Utilizamos una lista en vez de un mapa para definir las relaciones...
	static belongsTo = [Categoria2, Elemento]
	// ...y definimos el campo que hemos "perdido" de forma explícita.
	Categoria2 categoria2

	// Una categoría de nivel 3 puede contener muchos elementos.
	static hasMany = [elementos: Elemento]
}

Esta vez, como se indica en los comentarios del código, hemos definido el belongsTo utilizando una lista, por lo que Grails no se queja cuando indicamos elementos que no son pares “clave:valor“. Un problema resuelto. Pero, además, para no perder la bidireccionalidad que teníamos con nuestras relaciones 1:M, hemos definido explícitamente el campo que actúa como padre en nuestra jerarquía de categorías, y ahora sí todo funciona correctamente y nuestro esquema de BD se genera según lo esperado:

alter table categoria2 drop constraint FK4D47455FBCFBE91A;
alter table categoria3 drop constraint FK4D474560BCFC5D7A;
alter table elemento_categorias1 drop constraint FKA14DB945BCFBE91A;
alter table elemento_categorias1 drop constraint FKA14DB9452B924CFA;
alter table elemento_categorias2 drop constraint FKA14DB946BCFC5D7A;
alter table elemento_categorias2 drop constraint FKA14DB9462B924CFA;
alter table elemento_categorias3 drop constraint FKA14DB947BCFCD1DA;
alter table elemento_categorias3 drop constraint FKA14DB9472B924CFA;
drop table categoria1 if exists;
drop table categoria2 if exists;
drop table categoria3 if exists;
drop table elemento if exists;
drop table elemento_categorias1 if exists;
drop table elemento_categorias2 if exists;
drop table elemento_categorias3 if exists;
create table categoria1 (id bigint generated by default as identity (start with 1), version bigint not null, nombre varchar(255) not null, descripcion varchar(255), last_updated timestamp, date_created timestamp not null, primary key (id));
create table categoria2 (id bigint generated by default as identity (start with 1), version bigint not null, nombre varchar(255) not null, descripcion varchar(255), last_updated timestamp, date_created timestamp not null, categoria1_id bigint not null, primary key (id));
create table categoria3 (id bigint generated by default as identity (start with 1), version bigint not null, nombre varchar(255) not null, descripcion varchar(255), last_updated timestamp, date_created timestamp not null, categoria2_id bigint not null, primary key (id));
create table elemento (id bigint generated by default as identity (start with 1), version bigint not null, nombre varchar(255) not null, descripcion varchar(255) not null, primary key (id));
create table elemento_categorias1 (elemento_id bigint not null, categoria1_id bigint not null, primary key (elemento_id, categoria1_id));
create table elemento_categorias2 (categoria2_id bigint not null, elemento_id bigint not null, primary key (elemento_id, categoria2_id));
create table elemento_categorias3 (categoria3_id bigint not null, elemento_id bigint not null, primary key (elemento_id, categoria3_id));
alter table categoria2 add constraint FK4D47455FBCFBE91A foreign key (categoria1_id) references categoria1;
alter table categoria3 add constraint FK4D474560BCFC5D7A foreign key (categoria2_id) references categoria2;
alter table elemento_categorias1 add constraint FKA14DB945BCFBE91A foreign key (categoria1_id) references categoria1;
alter table elemento_categorias1 add constraint FKA14DB9452B924CFA foreign key (elemento_id) references elemento;
alter table elemento_categorias2 add constraint FKA14DB946BCFC5D7A foreign key (categoria2_id) references categoria2;
alter table elemento_categorias2 add constraint FKA14DB9462B924CFA foreign key (elemento_id) references elemento;
alter table elemento_categorias3 add constraint FKA14DB947BCFCD1DA foreign key (categoria3_id) references categoria3;
alter table elemento_categorias3 add constraint FKA14DB9472B924CFA foreign key (elemento_id) references elemento;

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!


  1. Juan

    Hola Enrique, me ha gustado tu artículo, ojalá lo hubiera leído antes de tener que descubrirlo por mí mismo, fue una dura mañana aquel día… xD

    No obstante hay un problema que echo de menos en tu artículo: ¿Qué pasa sí una clase A tiene una relación 1:N con una clase B, y otra relación N:N con la misma clase B? El belongsTo en forma de lista hace referencia solo a la clase, y por tanto no permite definir cuál o cuales de las relaciones que tienes con esa clase tienen la responsabilidad de la persistencia en cascada y cuáles no.

    Te pongo un ejemplo para que me entiendas: un departamento está formado por profesores que son miembros únicamente de un departamento (1:N). Pero además el departamento tiene un grupo de participantes que son profesores que pueden ser miembros de otro departamento y que pueden participar en varios departamentos (N:N). ¿Cómo establecer esas relaciones?

  2. Juan,

    El caso que presentas tiene su truco, pero puede ser resuelto. Antes de ponerte el código final, déjame que te presente el código inicial que uno podría pensar que es suficiente para poder realizar el mapeo que busca:

    class Profesor {

    String nombre

    // Departamento al que pertenece.
    static belongsTo = [departamento: Departamento]

    // Departamentos en los que participa.
    static hasMany = [departamentos: Departamento]

    static constraints = {
    }

    }

    class Departamento {

    String nombre

    // Profesores y participantes pertenecientes al departamento.
    static hasMany = [profesores: Profesor, participantes: Profesor]

    static constraints = {
    }

    }

    Si pruebas este código inicial, verás que Grails (bueno, en realidad Hibernate) se queja de que no se está indicando qué lado de la relación M:N es la ‘owner’ de la misma:

    Error loading plugin manager: No owner defined between domain classes [class Departamento] and [class Profesor] in a many-to-many relationship. Example: def belongsTo = Profesor
    at _PluginDependencies_groovy$_run_closure8_closure70.doCall(_PluginDependencies_groovy)
    at _GrailsSettings_groovy$_run_closure10.doCall(_GrailsSettings_groovy:287)
    at _GrailsSettings_groovy$_run_closure10.call(_GrailsSettings_groovy)
    at _PluginDependencies_groovy$_run_closure8.doCall(_PluginDependencies_groovy:596)
    at _GrailsBootstrap_groovy$_run_closure1.doCall(_GrailsBootstrap_groovy:69)

    Para solucionarlo, haríamos lo siguiente sólo en la clase Profesor:

    class Profesor {

    String nombre

    // Departamento al que pertenece.
    static belongsTo = Departamento

    // Departamentos en los que participa.
    static hasMany = [departamentos: Departamento]

    static constraints = {
    }

    }

    ¿Ves la diferencia? Hemos cambiado la forma de especificar el ‘belongsTo’. Ahora Grails ya no se queja, pero el resultado de esquema de BD que obtenemos no es el que esperábamos, ya que nos está generando también una relación M:N para el profesor que pertenece a un único departamento:

    create table departamento (id bigint generated by default as identity (start with 1), version bigint not null, nombre varchar(255) not null, primary key (id));
    create table departamento_participantes (departamento_id bigint not null, profesor_id bigint not null, primary key (departamento_id, profesor_id));
    create table departamento_profesores (profesor_id bigint not null, departamento_id bigint not null, primary key (departamento_id, profesor_id));
    create table profesor (id bigint generated by default as identity (start with 1), version bigint not null, nombre varchar(255) not null, primary key (id));
    alter table departamento_participantes add constraint FK1A3AA22640AF175A foreign key (departamento_id) references departamento;
    alter table departamento_participantes add constraint FK1A3AA226BA6159A foreign key (profesor_id) references profesor;
    alter table departamento_profesores add constraint FK7BCF6F3340AF175A foreign key (departamento_id) references departamento;
    alter table departamento_profesores add constraint FK7BCF6F33BA6159A foreign key (profesor_id) references profesor;

    ¿Cómo podemos entonces conseguir el resultado esperado entonces? Pues usando el ‘static mappedBy’ en la clase Departamento, y definiendo el campo ‘departamento’ de forma explícita en la clase Profesor:

    class Profesor {

    String nombre

    Departamento departamento

    // Departamento al que pertenece.
    static belongsTo = Departamento

    // Departamentos en los que participa.
    static hasMany = [departamentos: Departamento]

    static constraints = {
    }

    }

    class Departamento {

    String nombre

    // Profesores pertenecientes al departamento.
    static hasMany = [profesores: Profesor, participantes: Profesor]

    static mappedBy = [profesores: "departamento", participantes: "departamentos"]

    static constraints = {
    }

    }

    Ahora por fin cuando comprobamos el mapeo del esquema de BD, sí que obtenemos lo que queremos:

    create table departamento (id bigint generated by default as identity (start with 1), version bigint not null, nombre varchar(255) not null, primary key (id));
    create table departamento_participantes (profesor_id bigint not null, departamento_id bigint not null, primary key (departamento_id, profesor_id));
    create table profesor (id bigint generated by default as identity (start with 1), version bigint not null, nombre varchar(255) not null, departamento_id bigint not null, primary key (id));
    alter table departamento_participantes add constraint FK1A3AA22640AF175A foreign key (departamento_id) references departamento;
    alter table departamento_participantes add constraint FK1A3AA226BA6159A foreign key (profesor_id) references profesor;
    alter table profesor add constraint FKC440F5EA40AF175A foreign key (departamento_id) references departamento;

    Como siempre, Grails tiene el mecanismo para conseguir lo que queremos, sólo hay que saber aplicarlo en cada caso ;-)

  3. Juan

    Enhorabuena Enrique por tu buena exposición, ¡y gracias por la rapídisima respuesta! Ahora tu hilo queda mucho más completo ;)

    Para serte franco antes de escribir yo ya había llegado exactamente al mismo código que en tu respuesta (me costó un rato eso sí), pero pensaba que tal vez estaba haciendo algo mal o me faltaba algo pues tenía un problema a la hora de actualizar los grupos desde la vista de departamento. Implementé un select multiple para añadir y quitar profesores, pero aunque se pueden añadir sin problema, no los puedo quitar, a pesar de que la propiedad departamento del profesor es nullable.

    Simplemente el código básico (scaffolding) de la acción update del controlador ignora los profesores que has quitado en params.profesores (con participantes funciona estupendamente). Al final he tenido que implementar esa comprobación manualmente, e incluir una transacción para actualizar a null la propiedad departamento de todos los profesores que hemos quitado en el select multiple del departamento.

    Desearía que el método .save() fuese un poquito más listo en este sentido…

  4. Enrique Medina Montenegro

    Sí, creo haber leido hace poco en la lista de Grails algo parecido, sobre el problema de hacer ‘binding’ de una asociación y usarla directamente desde ‘params.xxxx’ (creo que publicado además por Marc Palmer, uno de los creadores de Grails).

  5. Álvaro

    Buenos días Enrique, este es de ese tipo de artículos que va a resultar fundamental para la gente que empieza, como yo. Estoy intentando modelar una relación 1:1 (se entiende la más sencilla) pero me estoy encontrando con un problema que no soy capaz de solventar. Al modelar la asociación, supongamos dos clases:

    Clase 1 {
    ….
    ….
    Static belongsTo = [clase2:Clase2]
    ….
    ….
    }
    Clase 2{
    ….
    ….
    Clase1 clase1
    ….
    ….
    }

    Indico que la clase que va a persistir es la 2. A la hora de introducir datos, cuando queremos introducir instancias de la clase 1 nos dice que no se puede crear ya que clase2 no puede ser nulo. Si incluyo una instancia de clase 2 en clase 1 poniendo Clase2 clase2 junto con el BelongsTo entro en un bucle sin fin, ya que si comienzo la aplicación sin ningún tipo de datos, no puedo crear instancias de la clase2 porque la 1 está vacía y viceversa.

    Estoy seguro que el problema tiene fácil solución, pero después de tanto tiempo peleandome no he dado con la tecla.

    Le agradezco de antemano su ayuda.

  6. Enrique Medina Montenegro

    Álvaro,

    Tal y como lo tienes configurado en el ejemplo que pones, para guardar instancias de la Clase1, debes crear una instancia de la Clase2 y guardarla a través de ella. Es decir:

    new Clase2(clase1: new Clase1()).save()

    Si lo haces al revés, obtienes el error.

  7. Buen truco Enrique, la verdad es que implementar una relación de 1:N y otra N:M sobre una misma clase tiene el problema del belongsTo en modo List y modo Map que siempre me pareció un misterio. Gracias!

  8. Enrique Medina Montenegro

    Gracias Alberto.

    Estamos preparando nuevos tutoriales para exprimir todavía un poco más el GORM, sobre todo a nivel de consultas.

    Estad atentos a las publicaciones del Observatorio.

  9. Hola a todos:

    Tengo dos clases, y una relación N-N entre éstas dos como muestro a continuación:

    class SessionChat {

    Integer tiempoSesion;
    Integer numeroParticipantes;
    static hasMany = [Usuarios:User]
    }
    class User {

    String name;
    static hasMany = [Sesiones:SessionChat]
    static belongsTo = SessionChat
    }
    Primero creo una instancia de sessionchat (lo hago bien) y luego quiero seleccionar alguna de las “sesiones” que he creado y relacionarlas con un nombre que introduce el usuario, creo que se hace asi:

    def a = SessionChat.get(seleccionarSesion[i]) //seleccionar[i] es un int
    .addToUsuarios(new User(name:nombreUsuario))
    .save()

    Pero me da error :( me dice que el metodo addToUsuarios no existe (y es verad) pero no se dónde debería crearlo ni como tendría que ser.

    Un saludo y muchas gracias por la ayuda!

  10. Enrique Medina Montenegro

    Javi,

    Tenemos habilitado un foro para este tipo de cuestiones. Regístrate y verás como enseguida alguien te responde a tu pregunta.

    Un saludo.

  11. william Canche Sulú

    Hola me estoy iniciado en Grails y me será de gran utilidad. Estaré atento

  12. Lorena

    Hola, tengo un problema al estaablecer la siguiente relación:

    SedeUniv tiene varios Turnos
    un Turno se puede asociar a varias SedeUniv

    Hasta aquí tengo una relacion M:N, pero quiero agregar

    Carrera está asociada a varias SedeUniv
    Carrera se dicta en Turnos-SedeUniv

    cómo establezco la ultima relación?
    tengo una relación M:N turno-SedeUniv
    otra relación M:N Carreta-SedeUniv
    y me falta la ultima que sería CarreraTurnoDeSedeUniv

  13. Lorena Castro

    Hola, tengo un problema al estaablecer la siguiente relación:

    SedeUniv tiene varios Turnos
    un Turno se puede asociar a varias SedeUniv

    Hasta aquí tengo una relacion M:N, pero quiero agregar

    Carrera está asociada a varias SedeUniv
    Carrera se dicta en Turnos-SedeUniv

    cómo establezco la ultima relación?
    tengo una relación M:N turno-SedeUniv
    otra relación M:N Carreta-SedeUniv
    y me falta la ultima que sería CarreraTurnoDeSedeUniv

  14. Enrique Medina Montenegro

    Lorena,

    Utiliza nuestro foro para plantear este tipo de dudas o preguntas, ya que tiene mayor visibilidad y permite hacer un mejor seguimiento.

    Cuando lo hagas, incluye algún ejemplo para que la gente entienda el modelo que estás intentando configurar, ya que parece más un problema de modelado relacional que de GORM en sí mismo.

    Un saludo.

Escribir un comentario


preload preload preload