O'Reilly logo

Harnessing Hibernate by James Elliott, Ryan Fowler, Timothy M. O'Brien

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. Collections and Associations

No, this isn’t about taxes or politics. Now that we’ve seen how easy it is to get individual objects into and out of a database, it’s time to see how to work with groups and relationships between objects. Happily, it’s no more difficult.

Mapping Collections

In any real application you’ll be managing lists and groups of things. Java provides a healthy and useful set of library classes to help with this: the Collections utilities. Hibernate provides natural ways for mapping database relationships onto Collections, which are usually very convenient. You do need to be aware of a couple semantic mismatches, generally minor. The biggest is the fact that Collections don’t provide “bag” semantics, which might frustrate some experienced database designers. This gap isn’t Hibernate’s fault, and it even makes some effort to work around the issue.

Note

Bags are like sets, except that the same value can appear more than once.

Enough abstraction! The Hibernate reference manual does a good job of discussing the whole bag issue, so let’s leave it and look at a working example of mapping a collection where the relational and Java models fit nicely. It might seem natural to build on the Track examples from Chapter 2 and group them into albums, but that’s not the simplest place to start, because organizing an album involves tracking additional information, like the disc on which the track is found (for multidisc albums), and other such finicky details. So let’s add artist information to our database.

Note

As usual, the examples assume you followed the steps in the previous chapters. If you did not, download the example source as a starting point.

The information of which we need to keep track for artists is, at least initially, pretty simple. We’ll start with just the artist’s name. And each track can be assigned a set of artists, so we know who to thank or blame for the music, and you can look up all tracks by an artist you like. (It really is critical to allow more than one artist to be assigned to a track, yet so few music management programs get this right. The task of adding a separate link to keep track of composers is left as a useful exercise for the reader after understanding this example.)

How do I do that?

For now, our Artist class doesn’t need anything other than a name property (and its key, of course). Setting up a mapping document for it will be easy. Create the file Artist.hbm.xml in the same directory as the Track mapping document, with the contents shown in Example 4-1.

Example 4-1. Mapping document for the Artist class

<?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="com.oreilly.hh.data.Artist" table="ARTIST">
    <meta attribute="class-description">
     Represents an artist who is associated with a track or album.
     @author Jim Elliott (with help from Hibernate)
    </meta>

    <id name="id" type="int" column="ARTIST_ID">
      <meta attribute="scope-set">protected</meta>
      <generator class="native"/>
    </id>

    <property name="name" type="string"> 1
      <meta attribute="use-in-tostring">true</meta>
      <column name="NAME" not-null="true" unique="true" index="ARTIST_NAME"/>
    </property>

    <set name="tracks" table="TRACK_ARTISTS" inverse="true"> 2
      <meta attribute="field-description">Tracks by this artist</meta> 3
      <key column="ARTIST_ID"/>
      <many-to-many class="com.oreilly.hh.data.Track" column="TRACK_ID"/>
    </set>

  </class>

</hibernate-mapping>
1

Our mapping for the name property introduces a couple of refinements to both the code generation and schema generation phases. The use-in-tostring meta tag causes the generated class to include a custom toString() method that shows the artist’s name as well as the cryptic hash code when it is printed, as an aid for debugging (you can see the result near the bottom of Example 4-4). And expanding the column attribute into a full-blown tag allows us finer-grained control over the nature of the column, which we use in this case to add an index for efficient lookup and sorting by name.

2

Notice that we can represent the fact that an artist is associated with one or more tracks quite naturally in this file. This mapping tells Hibernate to add a property named tracks to our Artist class, whose type is an implementation of java.util.Set. This will use a new table named TRACK_ARTISTS to link to the Track objects for which this Artist is responsible. The attribute inverse=true is explained later in the discussion of Example 4-3, where the bidirectional nature of this association is examined.

The TRACK_ARTISTS table we just called into existence will contain two columns: TRACK_ID and ARTIST_ID. Any rows appearing in this table will mean that the specified Artist object has something to do with the specified Track object. The fact that this information lives in its own table means that there is no restriction on how many tracks can be linked to a particular artist, nor how many artists are associated with a track. That’s what is meant by a “many-to-many” association[2].

On the flip side, since these links are in a separate table you have to perform a join query in order to retrieve any meaningful information about either the artists or the tracks. This is why such tables are often called “join tables.” Their whole purpose is to join other tables together.

Finally, notice that unlike the other tables we’ve set up in our schema, TRACK_ARTISTS does not correspond to any mapped Java object. It is used only to implement the links between Artist and Track objects, as reflected by Artist’s tracks property.

3

The field-description meta tag can be used to provide JavaDoc descriptions for collections and associations as well as plain old value fields. This is handy in situations where the field name isn’t completely self-documenting.

The tweaks and configuration choices provided by the mapping document, especially when aided by meta tags, give you a great deal of flexibility over how the source code and database schema are built. Nothing can quite compare to the control you can obtain by writing them yourself, but most common needs and scenarios appear to be within reach of the mapping-driven generation tools. This is great news, because they can save you a lot of tedious typing!

Once we’ve created Artist.hbm.xml we need to add it to the list of mapping resources in our hibernate.cfg.xml. Open up the hibernate.cfg.xml file in src, and add the line shown in bold in Example 4-2.

Example 4-2. Adding Artist.hbm.xml to the Hibernate configuration

<?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>
        
...        
    <mapping resource="com/oreilly/hh/data/Track.hbm.xml"/>
    <mapping resource="com/oreilly/hh/data/Artist.hbm.xml"/>

  </session-factory>
</hibernate-configuration>

With that in place, let’s also add the collection of Artists to our Track class. Edit Track.hbm.xml to include the new artists property as shown in Example 4-3 (the new content is shown in bold).

Example 4-3. Adding an artist collection to the Track mapping file

...

<property name="playTime" type="time">
  <meta attribute="field-description">Playing time</meta>
</property>

<set name="artists" table="TRACK_ARTISTS">
  <key column="TRACK_ID"/>
  <many-to-many class="com.oreilly.hh.data.Artist" column="ARTIST_ID"/>
</set>

<property name="added" type="date">
  <meta attribute="field-description">When the track was created</meta>
</property>

...

This adds a similar Set property named artists to the Track class. It uses the same TRACK_ARTISTS join table introduced earlier in Example 4-1 to link to the Artist objects we mapped there. This sort of bidirectional association is very useful. It’s important to let Hibernate know explicitly what’s going on by marking one end of the association as inverse. In the case of a many-to-many association like this one, the choice of which side to call the inverse mapping isn’t crucial, although it does affect when Hibernate will decide to automatically update the join table. The fact that the join table is named “track artists” makes the link from artists back to tracks the best choice for the inverse end, if only from the perspective of people trying to understand the database.

Hibernate itself doesn’t care which end we choose, as long as we mark one of the directions as inverse. That’s why we did so in Example 4-1. With this configuration, if we make changes to the artists set in a Track object, Hibernate will know it needs to update the TRACK_ARTISTS table. If we make changes to the tracks set in an Artist object, this will not automatically happen.

While we’re enhancing the Track mapping document we might as well flesh out the title property similar to how we fleshed out name in Artist:

...

<property name="title" type="string">
  <meta attribute="use-in-tostring">true</meta>
  <column name="TITLE" not-null="true" index="TRACK_TITLE"/>
</property>

...

With the new and updated mapping files in place, we’re ready to rerun ant codegen to update the Track source code, and create the new Artist source. If you do that and look at Track.java, you’ll see the new Set-valued property artists has been added, as has a new toString() method. Example 4-4 shows the content of the new Artist.java.

Example 4-4. Code generated for the Artist class

package com.oreilly.hh.data;
// Generated Sep 3, 2007 10:12:45 PM by Hibernate Tools 3.2.0.b9

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

/**
 *       Represents an artist who is associated with a track or album.
 *       @author Jim Elliott (with help from Hibernate)
 */
public class Artist  implements java.io.Serializable {

     private int id;
     private String name;
     /**
      * Tracks by this artist
     */
     private Set tracks = new HashSet(0);

    public Artist() {
    }

    public Artist(String name) {
        this.name = name;
    }

    public Artist(String name, Set tracks) {
       this.name = name;
       this.tracks = tracks;
    }
   
    public int getId() {
        return this.id;
    }
    
    protected void setId(int id) {
        this.id = id;
    }

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

    /**       
     *      * Tracks by this artist
     */
    public Set getTracks() {
        return this.tracks;
    }
    
    public void setTracks(Set tracks) {
        this.tracks = tracks;
    }

    /**
     * toString
     * @return String
     */
     public String toString() {
          StringBuffer buffer = new StringBuffer();

      buffer.append(getClass().getName()).append("@").append(
         Integer.toHexString(hashCode())).append(" [");
      buffer.append("name").append("='").append(getName()).append("' ");
      buffer.append("]");
      
      return buffer.toString();
     }
}

Note

Anyone looking for a popcorn project to pitch in on Hibernate? How about changing the tools’ code generation to use a StringBuilder rather than a StringBuffer in toString()?

Why didn’t it work?

If you see Hibernate complaining about something along these lines, don’t despair:

[hibernatetool] An exception occurred while running exporter 
#2:hbm2java (Generates a set of .java files)
[hibernatetool] To get the full stack trace run ant with -verbose
[hibernatetool] org.hibernate.MappingNotFoundException: resource: 
com/oreilly/hh/data/Track.hbm.xml not found
[hibernatetool] A resource located at com/oreilly/hh/data/Track.hbm.xml was not 
found.
[hibernatetool] Check the following:
[hibernatetool] 
[hibernatetool] 1) Is the spelling/casing correct ?
[hibernatetool] 2)      Is com/oreilly/hh/data/Track.hbm.xml available via the c
lasspath ?
[hibernatetool] 3) Does it actually exist ?

BUILD FAILED

This just means you were starting from a fresh download of the sample directory, and you’ve run into the kind of Ant premature classpath calcification issues we mentioned in “Cooking Up a Schema” back in Chapter 2. If you try it a second time (or if you had manually run ant prepare once before trying it in the first place), it should work fine.

What about…

…modern (Java 5) considerations of type-safety? These classes are using nongeneric versions of the Collections classes, and will cause compiler warnings like the following when code is compiled against them with current Java compilers:

    [javac] Note: /Users/jim/svn/oreilly/hib_dev_2e/current/scratch/ch04/src/com
/oreilly/hh/CreateTest.java uses unchecked or unsafe operations.
    [javac] Note: Recompile with -Xlint:unchecked for details.

It’d sure be nice if there was a way to generate code that used Java Generics, to tighten up the things we can put in the tracks Set and thereby avoid these warnings and all the tedious type casting that used to mar the Collections experience. Well, we’re in luck, because it’s actually pretty easy to do so, by modifying the way we invoke hbm2java. Edit build.xml and change the hmb2java line so that it looks like this:

      <hbm2java jdk5="true"/>

This tells the tool that we’re in a Java 5 (or later) environment, so we’d like to take advantage of its useful new capabilities. After this change, run ant codegen again, and notice the changes in the generated code, which are highlighted in Example 4-5.

Example 4-5. Improvements to generated Artist class in jdk5 mode

...
    private Set<Track> tracks = new HashSet<Track>(0);
...
    public Artist(String name, Set<Track> tracks) {
       this.name = name;
       this.tracks = tracks;
    }
...
    public Set<Track> getTracks() {
        return this.tracks;
    }
    
    public void setTracks(Set<Track> tracks) {
        this.tracks = tracks;
    }
...

This is the code I was hoping to see—nice type-safe use of the Java Generics capabilities added to Collections in Java 5. Similar treatment was given to the artists property in Track.java. Let’s take a peek at the new “full” constructor as an example, and so we can see how to invoke it later in Example 4-9:

    public Track(String title, String filePath, Date playTime,
                 Set<Artist> artists, Date added, short volume) {
...
    }

Note

Ah, much better. It’s amazing how much benefit you can get from one little configuration parameter.

Our schema change has created a new parameterized Set argument for the artists property between playTime and Date added.

Now that the classes are created (or updated), we can use ant schema to build the new database schema that supports them.

Tip

Of course you should watch for error messages when generating your source code and building your schema, in case there are any syntax or conceptual errors in the mapping document. Not all exceptions that show up are signs of real problems you need to address, though. In experimenting with evolving this schema, I ran into some exception reports because Hibernate tried to drop foreign key constraints that hadn’t been set up by previous runs. The schema generation continued past them, scary as they looked, and worked correctly. This may improve in later versions (of Hibernate or HSQLDB, or perhaps just the SQL dialect implementation), but the behavior has been around for several years now.

The generated schema contains the tables we’d expect, along with indices and some clever foreign key constraints. As our object model gets more sophisticated, the amount of work (and expertise) being provided by Hibernate is growing nicely. The full output from the schema generation is rather long, but Example 4-6 shows the highlights.

Example 4-6. Excerpts from our new schema generation

[hibernatetool] drop table ARTIST if exists;
[hibernatetool] drop table TRACK if exists;
[hibernatetool] drop table TRACK_ARTISTS if exists;

[hibernatetool] create table ARTIST (ARTIST_ID integer generated by default
  as identity (start with 1), NAME varchar(255) not null,
  primary key (ARTIST_ID), unique (NAME));

[hibernatetool] create table TRACK (TRACK_ID integer generated by default as
  identity (start with 1), TITLE varchar(255) not null,
  filePath varchar(255) not null, playTime time, added date,
  volume smallint not null, primary key (TRACK_ID));

[hibernatetool] create table TRACK_ARTISTS (ARTIST_ID integer not null,
  TRACK_ID integer not null, primary key (TRACK_ID, ARTIST_ID));

[hibernatetool] create index ARTIST_NAME on ARTIST (NAME);
[hibernatetool] create index TRACK_TITLE on TRACK (TITLE);

[hibernatetool] alter table TRACK_ARTISTS add constraint
  FK72EFDAD8620962DF foreign key (ARTIST_ID) references ARTIST;

[hibernatetool] alter table TRACK_ARTISTS add constraint
  FK72EFDAD82DCBFAB5 foreign key (TRACK_ID) references TRACK;

Note

Cool! I didn’t even know how to do some of that stuff in HSQLDB!

Figure 4-1 shows HSQLDB’s tree view representation of the schema after these additions. I’m not sure why two separate indices are used to establish the uniqueness constraint on artist names, but that seems to be an implementation quirk in HSQLDB, and this approach will work just fine.

HSQLDB graphical tree view of updated schema

Figure 4-1. HSQLDB graphical tree view of updated schema

What just happened?

We’ve set up an object model that allows our Track and Artist objects to keep track of an arbitrary set of relationships to each other. Any track can be associated with any number of artists, and any artist can be responsible for any number of tracks. Getting this set up right can be challenging, especially for people who are new to object-oriented code or relational databases (or both!), so it’s nice to have the help of Hibernate. But just wait until you see how easy it is to work with data in this setup.

It’s worth emphasizing that the links between artists and tracks are not stored in the ARTIST or TRACK tables themselves. Because they are in a many-to-many association, meaning that an artist can be associated with many tracks, and many artists can be associated with a track, these links are stored in a separate join table called TRACK_ARTISTS. Rows in this table pair an ARTIST_ID with a TRACK_ID, to indicate that the specified artist is associated with the specified track. By creating and deleting rows in this table, we can set up any pattern of associations we need. (This is how many-to-many relationships are always represented in relational databases; the chapter of George Reese’s Java Database Best Practices cited earlier is a good introduction to data models like this.)

Keeping this in mind, you will also notice that our generated classes don’t contain any code to manage the TRACK_ARTISTS table. Nor will the upcoming examples that create and link persistent Track and Artist objects. They don’t have to, because Hibernate’s special Collection classes take care of all those details for us, based on the mapping information we added to Examples 4-1 and 4-3.

All right, let’s create some tracks and artists.



[2] If concepts like join tables and many-to-many associations aren’t familiar, spending some time with a good data modeling introduction would be worthwhile. It will help a lot when it comes to designing, understanding, and talking about data-driven projects. George Reese’s Java Database Best Practices (O’Reilly) has such an introduction, and you can even view the chapter online at http://www.oreilly.com/catalog/javadtabp/chapter/ch02.pdf.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required