Tuesday, April 10, 2007

Mapping Associations


As we now have mapped a single object, we are now going to add various object
associations. For a starter, we will add users to our application, and store
a list of participating users with every event. In addition, we will give each
user the possibility to watch various events for updates. Every user will have
the standard personal data, including a list of email addresses.



Mapping the User class


For the beginning, our User class will be very simple:


User.java:



package de.gloegl.road2hibernate;

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

// ... getters and setters for the properties
// private getter again for the id property
}

And the mapping:


User.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>

<class name="de.gloegl.road2hibernate.User" table="USERS">
<id name="id" column="uid" type="long">

<generator class="increment"/>
</id>
<property name="age"/>
<property name="firstname"/>

<property name="lastname"/>
</class>

</hibernate-mapping>

The hibernate.cfg.xml needs to be adjusted as well to add the new resource:


hibernate.cfg.xml:



<?xml version='1.0' encoding='utf-8'?>

<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"

"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>

<session-factory>
<property name="hibernate.connection.driver_class">org.hsqldb.jdbcDriver</property>

<property name="hibernate.connection.url">jdbc:hsqldb:data/test</property>
<property name="hibernate.connection.username">sa</property>
<property name="hibernate.connection.password"></property>

<property name="dialect">org.hibernate.dialect.HSQLDialect</property>
<property name="show_sql">true</property>
<property name="transaction.factory_class">
org.hibernate.transaction.JDBCTransactionFactory
</property>

<property name="hibernate.cache.provider_class">
org.hibernate.cache.HashtableCacheProvider
</property>
<property name="hibernate.hbm2ddl.auto">update</property>

<mapping resource="de/gloegl/road2hibernate/Event.hbm.xml"/>

<mapping resource="de/gloegl/road2hibernate/User.hbm.xml"/>

</session-factory>

</hibernate-configuration>


A unidirectional Set-based association


So far this is only basic hibernate usage. But now we will add the collection
of favorite Events to the User class. For this we can use a simple java collection
- a Set in this case, because the collection will not contain duplicate elements
and the ordering is not relevant for us.


So our User class now looks like this:


User.java:



package de.gloegl.road2hibernate;

import java.util.Set;
import java.util.HashSet;

public class User {
private int age;
private String firstname;
private String lastname;
private Long id;
private Set favouriteEvents = new HashSet();

public Set getFavouriteEvents() {
return favouriteEvents;
}

public void setFavouriteEvents(Set newFavouriteEvents) {
favouriteEvents = newFavouriteEvents;
}

// mappings for the other properties.
}

Now we need to tell hibernate about the association, so we adjust the mapping
document:


User.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>

<class name="de.gloegl.road2hibernate.User" table="USERS">
<id name="id" column="uid" type="long">

<generator class="increment"/>
</id>
<property name="age"/>
<property name="firstname"/>

<property name="lastname"/>

<set name="favouriteEvents" table="favourite_events">
<key column="user_uid"/>

<many-to-many
column="event_uid"
class="de.gloegl.road2hibernate.Event"/>
</set>
</class>

</hibernate-mapping>


As you can see, we tell Hibernate about the set property called favouriteEvents.
The <set> element tells Hibernate that the collection property
is a Set. We have to consider what kind of association we have: Every User my
have multiple favorite Events, but every Event may be a favorite of multiple
Users. So we have a many-to-many association here, which we tell Hibernate using
the <many-to-many> tag. For many to many associations, we need
an association table where hibernate can store the associations. The table name
can be configured using the table attribute of the <set>
element. The association table needs at least two columns, one for every side
of the association. The column name for the User side can be configured using
the <key> element. The column name for the Event side is configured
using the column attribute of the <many-to-many> attribute.


So the relational model used by hibernate now looks like this:



_____________ __________________ _____________
| | | | | |
| EVENTS | | FAVOURITE_EVENTS | | USERS |
|_____________| |__________________| |_____________|
| | | | | |
| *UID | <--> | *EVENT_UID | | |
| DATE | | *USER_UID | <--> | *UID |
| EVENTTITLE | |__________________| | AGE |
|_____________| | FIRSTNAME |
| LASTNAME |
|_____________|



Modifying the association


As we now have mapped the association, modifying it is very easy:


from EventManager.java:



private void addFavouriteEvent(Long userId, Long eventId) {
Session session = HibernateUtil.currentSession();
Transaction tx = session.beginTransaction();

User user = (User) session.load(User.class, userId);
Event theEvent = (Event) session.load(Event.class, eventId);

user.getFavouriteEvents().add(theEvent);

tx.commit();
hsqlCleanup(session);
HibernateUtil.closeSession();
}

After loading an User and an Event with Hibernate, we can simply modify the
collection using the normal collection methods. As you can see, there is no
explicit call to session.update() or session.save(), Hibernate
automatically detects the collection has been modified and needs to be saved.


Sometimes however, we will have a User or an Event loaded in a different session.
This is of course possible to:


from EventManager.java:



private void addFavouriteEvent(Long userId, Long eventId) {
Session session = HibernateUtil.currentSession();
Transaction tx = session.beginTransaction();

User user = (User) session.load(User.class, userId);
Event theEvent = (Event) session.load(Event.class, eventId);

tx.commit();
HibernateUtil.closeSession();

session = HibernateUtil.currentSession();
tx = session.beginTransaction();

user.getFavouriteEvents().add(theEvent);

session.update(user);

tx.commit();
hsqlCleanup(session);
HibernateUtil.closeSession();
}

This time, we need an explicit call to update - Hibernate can't know if the
object actually changed since it was loaded in the previous session. So if we
have an object from an earlier session, we must update it explicitly. If the
object gets changed during session lifecycle we can rely on Hibernates automatic
dirty checking.


Since Hibernate 2.1 there is a third way - the object can be reassociated with
the new session using session.lock(object, LockMode.NONE):


from EventManager.java:



private void addFavouriteEvent(Long userId, Long eventId) {
Session session = HibernateUtil.currentSession();
Transaction tx = session.beginTransaction();

User user = (User) session.load(User.class, userId);
Event theEvent = (Event) session.load(Event.class, eventId);

tx.commit();
HibernateUtil.closeSession();

session = HibernateUtil.currentSession();
tx = session.beginTransaction();

session.lock(user, LockMode.NONE);

user.getFavouriteEvents().add(theEvent);

tx.commit();
hsqlCleanup(session);
HibernateUtil.closeSession();
}


Collections of Values


Often you will want to map collections of simple value types - like a collections
of Integers or a collection of Strings. We will do this for our User class with
a collection of Strings representing email addresses. So we add another Set
to our class:


User.java:



package de.gloegl.road2hibernate;

import java.util.Set;
import java.util.HashSet;

public class User {
private int age;
private String firstname;
private String lastname;
private Long id;
private Set favouriteEvents = new HashSet();
private Set emails = new HashSet();

public Set getEmails() {
return emails;
}

public void setEmails(Set newEmails) {
emails = newEmails;
}

// Other getters and setters ...
}


Next we will add the mapping of the Set to our mapping document:


User.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>

<class name="de.gloegl.road2hibernate.User" table="USERS">
<id name="id" column="uid" type="long">

<generator class="increment"/>
</id>
<property name="age"/>
<property name="firstname"/>

<property name="lastname"/>

<set name="favouriteEvents" table="favourite_events">
<key column="user_uid"/>

<many-to-many
column="event_uid"
class="de.gloegl.road2hibernate.Event"/>
</set>

<set name="emails" table="user_emails">

<key column="user_uid"/>
<element column="email" type="string"/>
</set>
</class>

</hibernate-mapping>

As you can see, the new set mapping looks a lot like the last one. The difference
is the <element> part, which tells Hibernate that the collection
does not contain an association with a mapped class, but a collection of elements
of type String. Once again, the table attribute of the <set>
element determines the table name. The <key> element determines
the column name in the user_emails table which establishes the relation to the
USERS table. The column attribute in the <element> element determines
the column name where the String values will be actually stored.


So now our relational model looks like this:



_____________ __________________ _____________ _____________
| | | | | | | |
| EVENTS | | FAVOURITE_EVENTS | | USERS | | USER_EMAILS |
|_____________| |__________________| |_____________| |_____________|
| | | | | | | |
| *UID | <--> | *EVENT_UID | | | | *ID |
| DATE | | *USER_UID | <--> | *UID | <--> | USER_UID |
| EVENTTITLE | |__________________| | AGE | | EMAIL |
|_____________| | FIRSTNAME | |_____________|
| LASTNAME |
|_____________|


Using value collections


Using value collections works the same way as we have already seen:


from EventManager.java:



private void addEmail(Long userId, String email) {
Session session = HibernateUtil.currentSession();
Transaction tx = session.beginTransaction();

User user = (User) session.load(User.class, userId);

user.getEmails().add(email);

tx.commit();
hsqlCleanup(session);
HibernateUtil.closeSession();
}

As you see, you can use the mapped collection just like every java collection.
Hibernates automatic dirty detection will do the rest of the job. For objects
from another session - or disconnected objects, as we will call them from now
- the same as above aplies. Explicitly update them, or reassociate them before
updating using session.lock(object, LockMode.NONE).



Bidirectional associations using Sets


Next we are going to map a bidirectional association - the User class will
contain a list of events where the user participates, and the Event class will
contain a list of participating users. So first we adjust our classes:


User.java:



package de.gloegl.road2hibernate;

import java.util.Set;
import java.util.HashSet;

public class User {
private int age;
private String firstname;
private String lastname;
private Long id;
private Set favouriteEvents = new HashSet();
private Set emails = new HashSet();
private Set eventsJoined = new HashSet();

public Set getEventsJoined() {
return eventsJoined;
}

public void setEventsJoined(Set newEventsJoined) {
eventsJoined = newEventsJoined;
}

// Other getters and setters ...
}

Event.java:



package de.gloegl.road2hibernate;

import java.util.Date;
import java.util.Set;
import java.util.HashSet;

public class Event {
private String title;
private Date date;
private Long id;
private Set participatingUsers = new HashSet();

private Set getParticipatingUsers() {
return participatingUsers;
}

private void setParticipatingUsers(Set newParticipatingUsers) {
participatingUsers = newParticipatingUsers;
}

// Other getters and setters ...
}

The mapping for a bidirectional association looks very much like a unidirectional
one, except the <set> elements are mapped for both classes:


Event.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>

<class name="de.gloegl.road2hibernate.Event" table="EVENTS">

<id name="id" column="uid" type="long">
<generator class="increment"/>
</id>

<property name="date" type="timestamp"/>
<property name="title" column="eventtitle"/>

<set name="participatingUsers" table="participations">

<key column="event_uid"/>
<many-to-many column="user_uid" class="de.gloegl.road2hibernate.User"/>
</set>
</class>

</hibernate-mapping>

User.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>

<class name="de.gloegl.road2hibernate.User" table="USERS">
<id name="id" column="uid" type="long">

<generator class="increment"/>
</id>
<property name="age"/>
<property name="firstname"/>

<property name="lastname"/>

<set name="favouriteEvents" table="favourite_events">
<key column="user_uid"/>

<many-to-many column="event_uid" class="de.gloegl.road2hibernate.Event"/>
</set>

<set name="emails" table="user_emails">

<key column="user_uid"/>
<element column="email" type="string"/>
</set>

<set name="eventsJoined" table="participations" inverse="true">

<key column="user_uid"/>
<many-to-many column="event_uid" class="de.gloegl.road2hibernate.Event"/>
</set>
</class>

</hibernate-mapping>

As you see, this are normal <set> mappings in both mapping documents.
Notice that the column names in <key> and <many-to-many>
are swapped in both mapping documents. The most important addition here is the
inverse="true" attribute in the <set> element of the
User mapping.


What this means is the other side - the Event class - will manage the relation.
So when only the Set in the User class is changed, this will not get perstisted.
Also when using explicit update for detatched objects, you need to update the
one not marked as inverse. Let's see an example:



Using bidirectional mappings


At first it is important to know that we are still responsible for keeping
our associations properly set up on the java side - that means if we add an
Event to the eventsJoined Set of an User object, we also have to add this User
object to the participatingUsers Set in the Event object. So we will add some
convenience methods to the Event class:


Event.java:



package de.gloegl.road2hibernate;

import java.util.Date;
import java.util.Set;
import java.util.HashSet;

public class Event {
private String title;
private Date date;
private Long id;
private Set participatingUsers = new HashSet();

protected Set getParticipatingUsers() {
return participatingUsers;
}

protected void setParticipatingUsers(Set newParticipatingUsers) {
participatingUsers = newParticipatingUsers;
}

public void addParticipant(User user) {
participatingUsers.add(user);
user.getEventsJoined().add(this);
}

public void removeParticipant(User user) {
participatingUsers.remove(user);
user.getEventsJoined().remove(this);
}

// Other getters and setters ...
}

Notice that the get and set methods for participatingUsers are now protected
- this allows classes in the same package and subclasses to still access the
methods, but prevents everybody else from messing around with the collections
directly. We should do the same to the getEventsJoined() and setEventsJoined()
methods in the User class.


Now using the association is very easy:


from EventManager.java:




private void addParticipant(Long userId, Long eventId) {
try {
Session session = HibernateUtil.currentSession();
Transaction tx = session.beginTransaction();

User user = (User) session.load(User.class, userId);
Event theEvent = (Event) session.load(Event.class, eventId);

theEvent.addParticipant(user);

tx.commit();
hsqlCleanup(session);
HibernateUtil.closeSession();
} catch (HibernateException e) {
throw new RuntimeException(e);
}
}

In the next chapter we will integrate Hibernate with Tomcat and WebWork to
create a better test environment - as you will notice when you look at the code,
the EventManager class is really ugly now.



Code download


You can download the part three development directory here

No comments: