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

Bidirectional List

Hibernate added the ability to have a bi-directional association on a list in relatively recent times (this works on version 3.2.2).
Making a list work on a bidirectional association is less intuitive than unidirectional lists, or bidirectional associations on other types of collection.

For this example, we are creating 2 simple entities: Singers and Albums.
The ID for singers will be assigned manually, and but the ID for the albums will be an autogenerated integer.
In addition, each album will have an index number.
We will manage the albums of a singer using a List object. We will add and remove albums from a singer, and verify if the list index of the album is maintained.

First, we create tables for Singer and Albums:
  create table ALBUM (ALBUM_ID integer not null, TITLE varchar(255), SINGER_ID integer not null, POSITION integer, primary key (ALBUM_ID))
  create table SINGER (SINGER_ID integer not null, NAME varchar(255), primary key (SINGER_ID))
  alter table ALBUM add constraint FKAlbumToSinger foreign key (SINGER_ID) references SINGER

Now the classes: Singer and Album.

Singer.java
package test9;

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

public class Singer {
	private int singerId;
	private List albums=new ArrayList();
	private String name;

	/**
	 * @return the singerId
	 */
	public int getSingerId() {
		return singerId;
	}

	/**
	 * @param accountId the accountId to set
	 */
	public void setSingerId(int id) {
		this.singerId = 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.setSinger(this);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

Since this is a bidirectional association, remember adding to the parent (Singer) a nice "addAlbum()" method, which sets the parent (itself) every time a new album is assigned to a singer.
Album.java
package test9;

public class Album {
	private int albumId;
	private String title;
	private Singer singer;

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

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

    public Singer getSinger() {
        return singer;
    }

    public void setSinger(Singer singer) {
        this.singer = singer;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

}


Finally, the mapping configuration file.
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 name=\"Singer\" table=\"SINGER\">
    <id name=\"singerId\" column=\"SINGER_ID\" type=\"int\"/>
    <property name=\"name\" column=\"NAME\" type=\"string\"/>
    <list name=\"albums\" lazy=\"true\" cascade=\"all\">
      <key column=\"SINGER_ID\" not-null=\"true\"/>
      <list-index column=\"POSITION\"/>
      <one-to-many class=\"Album\"/>
    </list>
  </class>

  <class name=\"Album\" table=\"ALBUM\">
    <id name=\"albumId\" column=\"ALBUM_ID\" type=\"int\">
      <generator class=\"increment\"/>
    </id>
    <property name=\"title\" column=\"TITLE\" type=\"string\"/>
    <many-to-one name=\"singer\" column=\"SINGER_ID\" class=\"Singer\" not-null=\"true\" insert=\"false\" update=\"false\" />
  </class>
</hibernate-mapping>
Now, several things to point out regardint this mapping file:
Notice how, despite being a bidirectional association, none of the sides has inverse="true".
Instead, the "many-to-one" side has the properties insert="false" and update="false".
This indicates that the list side, the "one-to-many" side, is the one in charge of performing the actual database operations. And this seems to go against the common practice in Hibernate, that the side containing the collection should always be the inactive one.
Perhaps this is being done this way because of the difficulty presented by persisting the list index.
Notice also the cascade="all" setting on the list. This saves me from having to persist every album indivisually once a singer is persisted.

Finally, some client code.
This code adds several albums to the singer Madonna, then it realizes that one of them (The Wall), doesn't belong to Madonna and removes it. Lastly, a new album is added.
Try stopping the execution after each session flush, and observe the generated SQL queries, and what are the database values for albums and index positions.
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 packageName){
		Configuration cfg=new Configuration();
		cfg.addResource(packageName +  "/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.username", "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();

		Singer singer=new Singer();
		singer.setName("Madonna");
		singer.setSingerId(1);

		//adding many
		Album album0=new Album();
		album0.setTitle("The Wall");
		Album album1=new Album();
		album1.setTitle("Like a Virgin");
		Album album2=new Album();
		album2.setTitle("True Blue");

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

		session.persist(singer);
		session.flush();


		//removing one
		singer.getAlbums().remove(album0);
		session.delete(album0);
		session.flush();



		//adding one last album
		Album album3=new Album();
		album3.setTitle("Confessions on a Dance Floor");
		singer.addAlbum(album3);
		session.persist(singer);
		session.flush();
	}

}