hibernar.org
A friendly forum for Hibernate users
<< Back to Articles

Listas bidireccionales

Recientemente, Hibernate agregó la capacidad de contar con listas bidireccionales (esto funciona, por lo menos, desde la versión 3.2.2).
Pero hacer que una lista funcione en una asociación bidireccional es un tanto menos intuitivo que en otros casas, sean conjuntos (sets) u otros tipos de colección bidireccional, o listas unidireccionales.

Para este ejemplo, creemos 2 simples entidades: Cantante y Álbum.
El ID de cantante será asignado manualmente por nosotros, pero el ID del álbum va a ser un entero autogenerado.
Además de su ID autogenerado, cada álbum tendrá un número de índice, comenzando de 0.
Los álbumes de un cantante, vamos a manejarlos un objeto Cantante, que tiene una propiedad List de objetos Album. En este ejemplo, vamos a agregar y quitar álbumes de la lista de álbumes de un cantante, y vamos a verificar que el índice de álbum se mantiene.

Primero creemos las tablas para Cantante y Álbum.
  create table ALBUM (ALBUM_ID integer not null, TITULO varchar(255), CANTANTE_ID integer not null, POSICION integer, primary key (ALBUM_ID))
  create table CANTANTE (CANTANTE_ID integer not null, NOMBRE varchar(255), primary key (CANTANTE_ID))
  alter table ALBUM add constraint FKAlbumACantante foreign key (CANTANTE_ID) references CANTANTE

Ahora las clasee, Cantante y Album

Cantante.java
package test9;

import java.util.ArrayList;
import java.util.List;

public class Cantante {
	private int cantanteId;
	private List albums=new ArrayList();
	private String nombre;

	/**
	 * @return the cantanteId
	 */
	public int getCantanteId() {
		return cantanteId;
	}

	/**
	 * @param accountId the accountId to set
	 */
	public void setCantanteId(int id) {
		this.cantanteId = id;
	}

    public List getAlbums() {
        return this.albums;
    }

    public void setAlbums(List albums) {
        this.albums = albums;
    }

    public void addAlbum(Album album){
        this.albums.add(album);
        album.setCantante(this);
    }

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

}

Como esta es una asociación bidireccional, recuerde agregarle al padre (Cantante) un bonito método "addAlbum()", el cual, además de agregar dicho álbum a la lista interna de Cantante, se asignará a sí mismo como padre del álbum.
Album.java
package test9;

public class Album {
	private int albumId;
	private String titulo;
	private Cantante cantante;

	/**
	 * @return the albumId
	 */
	public int getAlbumId() {
		return albumId;
	}

	/**
	 * @param albumId the albumId to set
	 */
	public void setAlbumId(int id) {
		this.albumId = id;
	}

    public Cantante getCantante() {
        return cantante;
    }

    public void setCantante(Cantante cantante) {
        this.cantante = cantante;
    }

    public String getTitulo() {
        return titulo;
    }

    public void setTitulo(String titulo) {
        this.titulo = titulo;
    }

}


Finalmente, el archivo de mapeo.
mapping.hbm.xml
<?xml version=\"1.0\"?>
<!DOCTYPE hibernate-mapping PUBLIC
	\"-//Hibernate/Hibernate Mapping DTD 3.0//EN\"
		\"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd\">

<hibernate-mapping package=\"test9\" >
   <class nombre=\"Cantante\" table=\"CANTANTE\">
    <id nombre=\"cantanteId\" column=\"CANTANTE_ID\" type=\"int\"/>
    <property nombre=\"nombre\" column=\"NOMBRE\" type=\"string\"/>
    <list nombre=\"albums\" lazy=\"true\" cascade=\"all\">
      <key column=\"CANTANTE_ID\" not-null=\"true\"/>
      <list-index column=\"POSICION\"/>
      <one-to-many class=\"Album\"/>
    </list>
  </class>

  <class nombre=\"Album\" table=\"ALBUM\">
    <id nombre=\"albumId\" column=\"ALBUM_ID\" type=\"int\">
      <generator class=\"increment\"/>
    </id>
    <property nombre=\"titulo\" column=\"TITLE\" type=\"string\"/>
    <many-to-one nombre=\"cantante\" column=\"CANTANTE_ID\" class=\"Cantante\" not-null=\"true\" insert=\"false\" update=\"false\" />
  </class>
</hibernate-mapping>
Hagamos notar varias cosas en relación a este archivito de mapeo:
Véase como, a pesar de que se trata de una asociación bidireccional, ninguno de los extremos de la asociación tiene la propiedad inverse=true, como habría de esperarse.
En lugar de eso, el lado "many-to-one" tiene las propiedades insert="false" y update="false".
Esto indica que el extremo de la lista, el "one-to-many", es en realidad el que se encarga de realizar el trabajo de base de datos, lo cual parece apartarse de lo que es la práctica común en Hibernate (que el extremo de la asociación que contiene la colección sea el inactivo).
Tal vez esto se deba a la dificultad que, internamente, plantea el escribir o mantener el índice en la base de datos.
Nótese también el la propiedad cascade="all" en la lista. Esto, aunque tal vez no sea muy eficiente, lo hago para ahorrarme el tener que persistir cada álbum individualmente en el ejemplo, prefiero simplemente persistir al padre.

Finalmente, un código cliente:
Este código le agrega varios álbumes a la cantante Madonna. Luego se da cuenta de que uno de los álbumes (The Wall), en realidad no le pertenece a Madonna, lo quita de la lista, y finalmente agrega un último álbum a la lista.
Para sacarle provecho a este ejemplo, trate de usar alguna herramienta de debug, o logueo, para observar, justo después de cada llamada a session,flush(), las consultas SQL que se van efectuando contra la base de datos.
Especialmente, los valores de la columna usada como índice de la lista (POSICION) en la tabla ALBUM.
package test9;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.tool.hbm2ddl.SchemaExport;


public class Main {

	private static Configuration configure(String packageNombre){
		Configuration cfg=new Configuration();
		cfg.addResource(packageNombre +  "/mapping.hbm.xml");
		cfg.setProperty("hibernate.dialect", "org.hibernate.dialect.MckoiDialect");
		cfg.setProperty("hibernate.connection.driver_class", "com.mckoi.JDBCDriver");
		cfg.setProperty("hibernate.connection.url", "jdbc:mckoi://localhost/app");
		cfg.setProperty("hibernate.connection.usernombre", "myuser");
		cfg.setProperty("hibernate.connection.password", "mypassword");
		cfg.setProperty("hibernate.connection.autocommit", "true");
		cfg.setProperty("hibernate.hbm2dll.auto", "drop_create");
		cfg.setProperty("hibernate.show_sql", "true");
		return cfg;
	}

	private static void createDB(Configuration cfg){
		SchemaExport se=new SchemaExport(cfg);
		se.create(true, true);
	}

	public static void main(String[] args){
		Configuration cfg=configure("test9");
		createDB(cfg);

		SessionFactory sef=cfg.buildSessionFactory();
		Session session=sef.openSession();

		Cantante cantante=new Cantante();
		cantante.setNombre("Madonna");
		cantante.setCantanteId(1);

		//agregando muchos
		Album album0=new Album();
		album0.setTitulo("The Wall");
		Album album1=new Album();
		album1.setTitulo("Like a Virgin");
		Album album2=new Album();
		album2.setTitulo("True Blue");

		cantante.addAlbum(album0);
		cantante.addAlbum(album1);
		cantante.addAlbum(album2);

		session.persist(cantante);
		session.flush();


		//borando uno
		cantante.getAlbums().remove(album0);
		session.delete(album0);
		session.flush();



		//agregando un último álbum
		Album album3=new Album();
		album3.setTitulo("Confessions on a Dance Floor");
		cantante.addAlbum(album3);
		session.persist(cantante);
		session.flush();
	}

}