viernes, 18 de septiembre de 2015

Asociaciones "many-to-many" en Hibernate

Asociaciones en Hibernate

Mapeo de asociaciones

En los ejemplos anteriores se ha realizado el mapeo de una sola entidad hacia una única tabla. A continuación ampliaremos los ejemplos teniendo en cuenta los distintos tipos de asociaciones entre clases. Tomaremos como ejemplo Personas que participan una serie de Eventos.

1.2.1. Mapeo de la clase Person

Una primera implementación de la clase  Person.java seria la siguiente:

	  package org.hibernate.tutorial.domain;

		public class Person {
		private Long id;
		private int age;
		private String firstname;
		private String lastname;

		public Person() {} // Accessor methods for all properties, private setter for 'id'

	}

Person.hbm.xml

<hibernate-mapping package="org.hibernate.tutorial.domain">
<class name="Person" table="PERSON">
<id name="id" column="PERSON_ID">
<generator class="native" />
</id>
<property name="age" />
<property name="firstname" />
<property name="lastname" />
</class>
</hibernate-mapping>

Dentro de hibernate.xml:

<mapping resource="org/hibernate/tutorial/domain/Event.hbm.xml" />
<mapping resource="org/hibernate/tutorial/domain/Person.hbm.xml" />

De esta manera hemos configurado la asociación bi-direccional entre ambas entidades. Los principios de diseño importantes a tener en cuenta son la direccionalidad, cardinalidad y el tipo de Collection que necesitamos.

1.2.2. Asociación uni-direccional basada en Set

Al añadir la collection de eventos a la clase  Person , es posible acceder a cada uno de los eventos para una instancia de persona, sin tener que ejecutar de forma explícita la consulta - llamada a  Person#getEvents. Hibernate mapea las asociaciones multi-valuadas a traves de una de las distintas implementaciones de Java Collection Framework; en este caso escogemos java.util.Set ya que no deseamos tener en cuenta elementos repetidos, ni su ordenación, en los ejemplos:

 public class Person {
	private Set events = new HashSet();

	public Set getEvents() {
		return events;
	}

	public void setEvents(Set events) {
		this.events = events;
	}
}
 

Antes de realizar el mapeo de la asociación, vemos que es posible permitir la bi-direccionalidad, creando otra colección de Person dentro de  Event. Esto no tiene por qué estar especificado desde el punto de vista funcional. Es posible implementar una consulta que de forma explicita obtenga todos los participantes dado un evento. Esta parte corresponde al diseño, de momento uni-direccional. Lo que ha de quedar claro, es que la asociación configurada implica una multiplicidad de muchos-a-muchos. Como vemos en el mapping Hibernate siguiente:

<class name="Person" table="PERSON">
<id name="id" column="PERSON_ID">
<generator class="native" />
</id>
<property name="age" />
<property name="firstname" />
<property name="lastname" />
<set name="events" table="PERSON_EVENT">
<key column="PERSON_ID" />
<many-to-many column="EVENT_ID" class="Event" />
</set>
</class>

Dentro del rango de mapeo a Collection,  set suele ser el más utilizado. Para las asociaciones muchos-a-muchos, y relaciones  n:m entre entidades, es necesaria la existencia de la tabla de asociacion. Dicha tabla se crea usando el nombre del atributo table del elemento set . La clave primaria en la tabla de asociación, se indica mediante el atributo key , los nombres de cada columna que se crean a partir de la entidad Event mediante el atributo column  de la configuración many-to-many. Hibernate acepta tambien que la declaracion de dicha collection sea tipada.

El esquema resultante del mapping sería el siguiente:

   _____________        __________________     |             |      |                  |       _____________   
     |EVENTS    |      |   PERSON_EVENT   |      |             |     |_____________|      |__________________|      
	 |PERSON   |     |             |      |                  |      |_____________|     | *EVENT_ID   |
   <--> 
   | *EVENT_ID        |      |             |     |  EVENT_DATE |      | *PERSON_ID       | 
   <--> 
   | *PERSON_ID  |     |  TITLE      |      |__________________|      |  AGE        |     |_____________|                                |  FIRSTNAME |                                                    |  LASTNAME   |                                                    |_____________|   
 

1.2.3. Trabajando sobre la asociation many-to-many

Creamos una clase EventManager que nos permita realizar ciertas operaciones sobre las entidades:

  	private void addPersonToEvent(Long personId, Long eventId) {
		Session session = HibernateUtil.getSessionFactory().getCurrentSession();
		session.beginTransaction();
		Person aPerson = (Person) session.load(Person.class, personId);
		Event anEvent = (Event) session.load(Event.class, eventId);
		aPerson.getEvents().add(anEvent);
		session.getTransaction().commit();
	}
   

Actualizaremos los valores de la colección a través de los métodos que ofrece. Como vemos, no existe una llamada explicita a los métodos  update() o save(); el framework Hibernate detecta automaticamente que la colección se ha modificado y necesita actualizarse (update). A ésto se le llama "automatic dirty checking". Pasar a "dirty" es posible, por ejemplo, modificando cualquier atributo (name, date...) de cualquiera de los objetos entidad. Mientras esten en su estado persistent, reflejado en la session una Hibernate org.hibernate.Session correspondiente, el framework Hibernate controla cualquier cambio y ejecuta sentencias SQL de escritura en segundo plano. El proceso de sincronización del estado de la memoria con la base de datos, que normalmente se ejecuta al final de cada unidad de operación, se denomina flushing. En el código, la finalización de la unidad de operación, se corresponde con la llamada a commit, o ejecución de rollback, dentro de una transacción.

Es posible realizar la carga de los datos de una entidad person y sus events multiples unidades de operación. Además es posible actualizar un objeto fuera de su org.hibernate.Session , cuando no se encuentra en un estado "persistent" (si se encontraba en el estado persistent antes, este nuevo estado se denomina " detached"). En el estado "detached" es posible además modificar los datos de una colección (ej. Events):

  private void addPersonToEvent(Long personId, Long eventId) { 
  Session session = HibernateUtil.getSessionFactory().getCurrentSession(); 		              session.beginTransaction(); //
Person aPerson = (Person)  session
.createQuery("select p from Person p left join fetch p.events where p.id = :pid")
.setParameter("pid", personId).uniqueResult();// Eager fetch the collection so we can use it detached  
   Event anEvent = (Event) session.load(Event.class, eventId); 		      session.getTransaction().commit();  // End of first unit of work        
    aPerson.getEvents().add(anEvent); // aPerson (and its collection) is detached
    // Begin second unit of work            
     Session session2 = HibernateUtil.getSessionFactory().getCurrentSession();       
     session2.beginTransaction(); 
	 sesion2.update(aPerson); // Reattachment of aPerson         
	 session2.getTransaction().commit();
}
	 	
   

La llamada al método update hace que el objeto pase de "detached" a "persistent" de nuevo, asociándolo a una nueva unidad de operación, de esta forma cualquier modificación que hubiesemos realizado cuando estaba en "detached", se guarda en la base de datos. Esto afecta a cualquier tipo modificación que se hubiese realizado (inserciones/borrados) a la colección (ej. events) de dicha entidad (ej. person).

Aunque no es de mucha utilidad en nuestro ejemplo, se trata de un concepto muy importante que debe ser tenido en cuenta a la hora del diseño. Completaremos el tutorial, añadirendo una nueva acción a la clase manager EventManager y realizaremos una llamada desde la linea de comandos. Para recuperar los identificadores de una persona y de un evento - el método save() devuelve el id:

   else if (args[0].equals("addpersontoevent")) { 
		Long eventId = mgr.createAndStoreEvent("My Event", new Date());
		Long personId = mgr.createAndStorePerson("Foo", "Bar"); 
		mgr.addPersonToEvent(personId, eventId);
		System.out.println("Added person " + personId + " to event " + eventId);       
		}

Este es un ejemplo de asociación entre dos clases de igual relevancia (es decir dos entidades). Como mencionabamos anteriormente, existen otros tipos de clases y tipos, dentro de un modelo, normalmente de menor relevancia en el diseño. Por ejemplo, podriamos tener una clase Address dentro de Person, o un tipo de valor long para Event indicando el timestamp en el que ocurre. Ambos valores dependen de su entidad.

En el diseño puede aparecer una entidad independiente como colección de valores estáticos. Lo cual es conceptualmente distinto a una collection referenciada entre entidades aunque parezca lo mísmo en código.

1.2.4. Añadir una colection de valores

Añadiremos la colección de direcciones email a la entidad  Person. Para ello usaremos java.util.Set de java.lang.String :

private Set emailAddresses = new HashSet();

	public Set getEmailAddresses() {
		return emailAddresses;
	}

	public void setEmailAddresses(Set emailAddresses) {
		this.emailAddresses = emailAddresses;
	}

El mapeo de la colección se realiza de la siguiente manera:

private Set emailAddresses = new HashSet();

	public Set getEmailAddresses() {
		return emailAddresses;
	}

	public void setEmailAddresses(Set emailAddresses) {
		this.emailAddresses = emailAddresses;
	}
	

La diferencia entre el mapeo anterior es la utilización de element lo que indica a Hibernate que dicha colección no referencia otra entidad sino que se trata de un conjunto de valores (en este caso de tipo string), mediante type=string indicamos el conversor de tipo. De igual manera el atributo table del elemento  set se corrresponde con el nombre de la tabla.  key indica el nombre de la columna que se usara como foreign-key en dicha tabal. El atributo column de  element nos indica el nombre de la columna en la que se almacenan los valores en base de datos.

Vemos el esquema resultante:

  _____________        __________________   |             |      |                  |       _____________   |   EVENTS    |      |   PERSON_EVENT   |      |             |       ___________________   |_____________|      |__________________|      |    PERSON   |      |                   |   |             |      |                  |      |_____________|      | PERSON_EMAIL_ADDR |   | *EVENT_ID   | 
   <-->
    | *EVENT_ID        |      |             |      |___________________|   |  EVENT_DATE |      | *PERSON_ID       | 
	<-->
	 | *PERSON_ID  | 
	 <--> 
	|  *PERSON_ID       |   |  TITLE      |      
	|__________________|      |  AGE        |      |  *EMAIL_ADDR      |   |_____________| 
	                               |  FIRSTNAME  |    
	  |___________________| |  LASTNAME   |                                         
	  |_____________|  
	   

Se puede ver que la primary key de la tabla de valores correspondiente a la colección es una clave compuesta de ambas columnas. Lo que implica que no pueden existir valores duplicados de email para una persona. Esto es lo que funcionalmente se deseaba y se indico con la Collection java.util.Set en Java.

Vemos la implementación de un método para añadir elementos a la colección, como se hizo anteriormente.

	private void addEmailToPerson(Long personId, String emailAddress) {
		Session session = HibernateUtil.getSessionFactory().getCurrentSession();
		session.beginTransaction();
		Person aPerson = (Person) session.load(Person.class, personId); 
		aPerson.getEmailAddresses().add(emailAddress);
		session.getTransaction().commit();
	}
  

Esta vez no se ha utilizado  fetch query para initializar la colección. Viendo el log de la consulta, intentaremos optimizarlo mediante eager fetch.

1.2.5. Asociación bi-directional

A continuación realizaremos el mapping de una asociación bi-direccional. El esquema creado, no cambia, ya que necesitamos la multiplicidad many-to-many.

Importante

Tener en cuenta que en la navegabilidad es más flexible desde el punto de vista de acceso a una base de datos relacional que el caso de acceso de forma remota; en estos casos seria necesario tener en cuenta la navegabilidad, desde el punto de vista de rendimiento.

Añadimos, la colección de participants a la clase Event :

       private Set participants = new HashSet();

	public Set getParticipants() {
		return participants;
	}

	public void setParticipants(Set participants) {
		this.participants = participants;
	}
	   

Creamos el mapping para la asociación,  Event.hbm.xml

   <set name="participants" table="PERSON_EVENT" inverse="true">
<key column="EVENT_ID" />
<many-to-many column="PERSON_ID" class="Person" />
</set>

Realizamos dicho mapping a través de set en ambos ficheros. Como vemos, los nombres de las columnas dentro de  key y many-to-many, estan intercambiados en ambos documentos .hbm.xml. Lo más importante a tener en cuenta es el atributo inverse="true" dentro del elemento set para el mapping de Event.

Esto indica a Hibernate que ha de acceder a la clase Person , cada vez que necesite información acerca de la relación entre el Event en curso y los participantes.

1.2.6. Trabajando con asociaciones bi-directionales

Tengamos en cuenta que Hibernate no modifica la semantica de Java. Entonces, cómo se enlazan las clases Person y Event en el ejemplo de navegabilidad unidireccional? Al añadir una instancia de  Event a la colección events, dentro de una instancia Person. Si queremos que dicho enlace sea bi-directional, lo que debemos hacer es añadir en una referencia a una collection Person dentro de Event. "Crear un enlace en ambos lados" es absolutamente necesario si queremos que dicho enlace sea bi-direccional.

Su acceso bi-direccional en  Person:

 	protected Set getEvents() {
			return events;
			}

	protected void setEvents(Set events) {
		this.events = events;
	}

	public void addToEvent(Event event) {
		this.getEvents().add(event);
		event.getParticipants().add(this);
	}

	public void removeFromEvent(Event event) {
		this.getEvents().remove(event);
		event.getParticipants().remove(this);
	}
	   

Vemos que los métodos get y set para el manejo de la coleccion son protected. Esto permite que las clases del mismo paquete y sus subclases puedan acceder, evitando que clases fuera del paquete puedan modificarla. Esto se aplica tambien al otro lado (Event).

Respecto al atributo  inverse , en el caso de un enlace bi-directional se trata únicamente de establecer las referencias en ambos lados. Sin embargo, Hibernate necesita más información para realizar las operaciones de INSERT y UPDATE  (y así evitar constraint violations). Si establecemos uno de los lados como inverse  Hibernate lo considerará como  mirror (copia exacta del otro lado). Una vez indicado esto, Hibernate es capaz de resolver la transformación direccional de los objetos enlazados al esquema SQL. Esta configuración es fija: Toda asociación bi-direccional a de tener uno de los lados inverse.