Yes, wealthy friends would be nice. But we can’t propose an easy way
to get any, so instead let’s look at relationships between objects
that carry more information than simple grouping. In this chapter
we’ll look at the tracks that make up an album. We put that off in Chapter 4 because
organizing an album involves more than simply grouping some tracks; you also
need to know the order in which the tracks occur, as well as things
like which disc they’re on, if you want to support multidisc
albums. That goes beyond what you can achieve with an automatically
generated join table, so we’ll design our own
AlbumTrack
object and table, and let albums link to
these.
First rich, then eager and lazy? It sure sounds like we’re anthropomorphizing our data model. But this really is an object/relational mapping topic of some importance. As your data model grows, adding associations between objects and tables, your program gains power, which is great. But you often end up with a large fraction of your objects somehow linked to each other. So, what happens when you load one of the objects that is part of a huge interrelated cluster? Since, as you’ve seen, you can move from one object to its associated objects just by traversing properties, it seems you’d have to load all the associated objects when you load any of them. For small databases, this is fine (and indeed, the HSQLDB databases we’ve been playing with in the examples exist entirely in memory at runtime), but in general application design your database will often hold a lot more than the memory available to your program. Uh oh! And, even if it could all fit, rarely will you actually access most of those objects, so it’d be a waste to load them all.
Luckily, this problem was anticipated by the designers of object/relational mapping software, including Hibernate. The trick is to allow associations to be “lazy,” so that associated objects aren’t loaded until they’re actually referenced. Hibernate will instead make a note of the linked object’s identity and put off loading it until you actually try to access it. This is especially important for collections like those we’ve been using.
Prior to Hibernate 3, the default was for associations
not to be lazy, so you needed to set the lazy
attribute in the mapping declaration.
However, since it is almost always the best practice to use lazy
associations, this default was reversed in Hibernate 3 (which required
careful attention on the part of people migrating their applications;
backward-incompatible changes like this are never undertaken
lightly).
When their associations are lazy, Hibernate uses its own special lazy implementations of Collections classes that don’t load their contents from the database until you actually try to use them. This is done completely transparently, so you don’t even notice it’s taking place in your code.
Well, if it’s that simple, and avoids problems with loading giant
snarls of interrelated objects, why would you ever turn it off? The
problem is that the transparency breaks down once you’ve closed your
Hibernate session. At that point, if you try to access content from a
lazy collection that hasn’t been initialized (even if you’ve assigned
the collection to a different variable, or returned it from a method
call), the Hibernate-provided proxy collection can no longer access the
database to perform the deferred loading of its contents, and it is
forced to throw a LazyInitializationException
.
(And, as we’ll discuss in a few pages, you do want to close your
sessions quickly.)
Conservation of complexity seems almost like a law of thermodynamics.
Because this can lead to unexpected crashes far away from the Hibernate-specific code, you can turn it off in cases where the effort of making sure anything you might need to use outweighs the potential cost of loading more than you really need. It’s your responsibility to think carefully about situations in which you need to disable it, and ensure that you are using your lazily-loaded objects safely. The Hibernate reference manual goes into a bit of detail about strategies to consider.
For example, if we wanted to avoid having to worry about lazy initialization, our track artists mapping could look like Example 5-1.
…laziness outside of collections? Caching and clustering?
It’s easy to see how lazy collections can be supported, since
Hibernate can provide its own special implementations of the various
Collection
interfaces. But what about other kinds
of associations? They might benefit from on-demand loading as
well.
In fact, Hibernate does support this, and almost as easily (at
least from our perspective as users of its services). Again, starting
with Hibernate 3, this is enabled by default but you can turn it off by
marking an entire persistent class as lazy
=
false
(this attribute goes right in the
class
tag of the mapping
document).
When your classes are being mapped in a lazy fashion, Hibernate will generate a proxy class that extends (and poses as) your data class. This lazy proxy puts off actually loading the data until it is needed. Any other objects with associations to the lazy class will sneakily be given these proxy objects, rather than references to your actual data object. The first time any of the methods of the proxy object are used, it will load the real data object and delegate the method call to it. Once the data object is loaded, the proxy simply continues delegating all method calls to it.
If you want to get fancier, you can specify a specific class (or
interface) to be extended (or implemented) by the proxy class, using the
proxy
attribute. The lazy
attribute is shorthand for specifying
the persistent class itself as the type to be proxied. (If this is all
incomprehensible, don’t worry; that just means you don’t yet need this
capability. By the time you do, you’ll understand it!)
Naturally, the same caveats about taking care to load anything you’ll need to use before closing the session also apply to this kind of lazy initialization. You can use it, but do so with care and planning.
The Hibernate reference documentation discusses these
considerations in more depth in its chapter Improving
Performance. Also introduced there is the fact that
Hibernate can be integrated with JVM-level or even clustered object
caches to boost the performances of large, distributed applications, by
reducing the bottleneck of database access. When plugged in to such a
cache, the mapping document lets you configure the cache behavior of
classes and associations using (appropriately enough) cache
tags. These configurations go well
beyond what we cover in this book, but you should be aware that they’re
possible in case your application would benefit from them.
You may not have enjoyed even skimming that discussion. Frankly, these issues of understanding the boundaries of the set of objects your code will need to access along any path it might execute, and trying to optimize the ones that are loaded without wasting too much memory or developer energy, are among the most difficult tradeoffs and challenges that exist in object/relational mapping. Not even a nice library like Hibernate can completely shield you from them.
Fortunately, there are techniques for structuring your data access code to essentially avoid the whole problem in many common application scenarios, and you can probably understand why they are so popular. Remember that lazy associations can bite you only when you try to traverse the association after you have already closed the Hibernate session. If you do all your data manipulation while you have the session open, lazy associations always work perfectly and you don’t have to think about them.
OK, great, let’s just open a session at the start of the application, and close it when the user quits, right? Well, no, not so fast. A session includes a database transaction, and databases are shared resources. The longer you keep open a transaction, the more you bog down the database, the longer you hide your activities from other users and processes that probably should see them, and the more likely you’ll run into a conflict with some other transaction when you finally try to commit it. You would never keep a transaction open while waiting for the user to do something.
So are we stuck in a catch-22? It’s not as bad as all that. Often the overall structure of an application makes it very clear when data access is taking place, and provides natural boundaries for the Hibernate session to begin and end. For example, in a web application, it makes great sense to open a Hibernate session for the duration of processing an incoming request. In fact, people often set up Servlet filters to do just that automatically. And, if there are background tasks which periodically work with the data (for example to send out mailings overnight), they can use their own separate sessions with similarly well-defined boundaries. So, it’s tricky and important, but not insurmountable. We provide more concrete examples of good approaches in Chapters 13 and 14.
That was a pretty big digression about object lifecycles, and an important one. But this chapter is meant to teach you how to do other fancy tricks with mapped collections, so let’s get back to that fun stuff! Our first goal will be to store the tracks that make up an album, keeping them in the right order. Later we’ll add information like the disc on which a track is found, and its position on that disc, so we can gracefully handle multidisc albums.
Oh, right, that’s what we were going to try....
The task of keeping a collection in a particular order is actually
quite straightforward. If that’s all we cared about in organizing album
tracks, we’d need only tell Hibernate to map a
List
or an Array
. In our
Album
mapping we’d use something like what’s
shown in Example 5-2.
<list name="tracks" table="ALBUM_TRACKS"> <key column="ALBUM_ID"/> <list-index column="LIST_POS"/> <many-to-many class="com.oreilly.hh.data.Track" column="TRACK_ID"/> </list>
This is very much like the set mappings we’ve used so far
(although it uses a different tag to indicate that it’s an ordered
list
and therefore maps to a
java.util.List
). But notice that we also need to
add a list-index
tag to establish the
ordering of the list, and we need to add a column to hold the value
controlling the ordering in the database. Hibernate will manage the
contents of this column for us, and use it to ensure that when we get
the list out of the database in the future, its contents will be in the
same order in which we stored them. The column is created as an integer,
and if possible, it is used as part of a composite key for the table.
The mapping in Example 5-2, when used to generate a HSQLDB database
schema, produces the table shown in Example 5-3.
[hibernatetool] create table ALBUM_TRACKS (ALBUM_ID INTEGER not null, TRACK_ID INTEGER not null, LIST_POS INTEGER not null, primary key (ALBUM_ID, LIST_POS))
It’s important to understand why the LIST_POS
column is necessary. We need to
control the order in which tracks appear in an album, and there aren’t
any properties of the tracks themselves we can use to keep them sorted
in the right order. (Imagine how annoyed you’d be if your jukebox system
could only play the tracks of an album in, say, alphabetical order,
regardless of the intent of the artists who created it!) The fundamental
nature of relational database systems is that you get results in
whatever order the system finds convenient, unless you tell it how to
sort them. The LIST_POS
column gives
Hibernate a value under its control that can be used to ensure that our
list is always sorted in the order in which we created it. Another way
to think about this is that the order of the entries is one of the
independent pieces of information of which we want to keep track, so
Hibernate needs a place to store it.
The corollary is also important. If there are values in your data
that provide a natural order for traversal, there is no need for you to
provide an index column; you don’t even have to use a list
. The set
and map
collection mappings can be configured to be sorted in Java by providing
a sort
attribute, or within the
database itself by providing a SQL order-by
attribute[4]. In either case, when you iterate over the contents of
the collection, you’ll get them in the
specified order.
The values in the LIST_POS
column will always be the same values you’d use as an
argument to the tracks.get()
method in
order to obtain the value at a particular position in the tracks list.
All right, we’ve got a handle on what we need to do if we want our albums’ tracks to be kept in the right order. What
about the additional information we’d like to keep, such as the disc on
which the track is found? When we map a collection of associations, we’ve
seen that Hibernate creates a join table in which to store the
relationships between objects. And we’ve just seen how to add an index
column to the ALBUM_TRACKS
table to
maintain an ordering for the collection. Ideally, we’d like to have the
ability to augment that table with more information of our own choosing,
in order to record the other details we’d like to know about album
tracks.
As it turns out, we can do just that, and in a very straightforward way.
Up to this point we’ve seen two ways of getting tables into our database schema. The first was by explicitly mapping properties of a Java object onto columns of a table. The second was defining a collection (of values or associations), and specifying the table and columns used to manage that collection. As it turns out, there’s nothing that prevents us from using a single table in both ways. Some of its columns can be used directly to map to our own objects’ properties, while the others can manage the mapping of a collection. This lets us achieve our goals of recording the tracks that make up an album in an ordered way, augmented by additional details to support multidisc albums.
This flexibility takes a little getting used to, but it makes sense, especially if you think about mapping objects to an existing database schema.
We’ll want a new data object, AlbumTrack
,
to contain information about how a track is used on an album. Since
we’ve already seen several examples of how to map full-blown entities
with independent existence, and there really isn’t a need for our
AlbumTrack
object to exist outside
the context of an Album
entity, this is a good
opportunity to look at mapping a
component. Recall that in Hibernate jargon an entity is an object
that stands on its own in the persistence mechanism: it can be created,
queried, and deleted independently of any other objects, and therefore
has its own persistent identity (as reflected by its mandatory id property). A component, in contrast, is an
object that can be saved to and retrieved from the database, but only as
a subordinate part of some other entity. In this case, we’ll define a
list of AlbumTrack
objects as a component part of
our Album
entity. Example 5-4 shows a
mapping for the Album
class that achieves
this.
<?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.Album" table="ALBUM"> <meta attribute="class-description"> Represents an album in the music database, an organized list of tracks. @author Jim Elliott (with help from Hibernate) </meta> <id column="ALBUM_ID" name="id" type="int"> <meta attribute="scope-set">protected</meta> <generator class="native" /> </id> <property name="title" type="string"> <meta attribute="use-in-tostring">true</meta> <column index="ALBUM_TITLE" name="TITLE" not-null="true" /> </property> <property name="numDiscs" type="integer" /> <set name="artists" table="ALBUM_ARTISTS"> <key column="ALBUM_ID" /> <many-to-many class="com.oreilly.hh.data.Artist" column="ARTIST_ID" /> </set> <set name="comments" table="ALBUM_COMMENTS"> <key column="ALBUM_ID" /> <element column="COMMENT" type="string" /> </set> <list name="tracks" table="ALBUM_TRACKS"> <meta attribute="use-in-tostring">true</meta> <key column="ALBUM_ID" /> <index column="LIST_POS" /> <composite-element class="com.oreilly.hh.data.AlbumTrack"> <many-to-one class="com.oreilly.hh.data.Track" name="track"> <meta attribute="use-in-tostring">true</meta> <column name="TRACK_ID" /> </many-to-one> <property name="disc" type="integer" /> <property name="positionOnDisc" type="integer" /> </composite-element> </list> <property name="added" type="date"> <meta attribute="field-description"> When the album was created </meta> </property> </class> </hibernate-mapping>
Once we’ve created the file Album.hbm.xml, we need to add it to the list of mapping resources in hibernate.cfg.xml. Open up the hibernate.cfg.xml file in src, and add the line highlighted in bold in Example 5-5.
<?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"/>
<mapping resource="com/oreilly/hh/data/Album.hbm.xml"/>
</session-factory>
</hibernate-configuration>
A lot of this is similar to mappings we’ve seen before, but the tracks list is worth some careful examination. The discussion gets involved, so let’s step back a minute and recall exactly what we’re trying to accomplish.
We want our album to keep an ordered list of the tracks that make it up, along with additional information about each track that tells which disc it’s on (in case the album has multiple discs) and the track’s position within the disc. This conceptual relationship is shown in the middle of Figure 5-1. The association between albums and tracks is mediated by an “Album Tracks” object that adds disc and position information, as well as keeping them in the right order. The model of the tracks themselves is familiar (we’re leaving out artist and comment information in this diagram, in an effort to keep it simple). This model is what we’ve captured in the album mapping document, Example 5-4. Let’s examine the details of how it was done. Later we’ll look at how Hibernate turns this specification into Java code (the bottom part of Figure 5-1) and a database schema (the top part).
All right, armed with this reminder and elaboration of the conceptual framework, we’re ready to look at the details of Example 5-4:
If you compare the list
definition with one of the set
mappings in the preceding chapter, you’ll see a lot of similarity.
It looks even more like Example 5-2, except that the association mapping
has been moved inside a new composite-element
mapping.
This element introduces the new
AlbumTrack
object we use to group the disc,
position, and Track
link needed to organize
an album’s tracks.
Also, rather than being a many-to-many mapping (because an album generally has multiple tracks, and
a given track file might be shared between several albums), the
association between AlbumTrack
and
Track
is many-to-one: several
AlbumTrack
objects (from different albums)
might refer to the same Track
file if we’re
trying to save disk space, but each
AlbumTrack
object is concerned with only one
Track
. The list
tag that contains AlbumTrack
is implicitly
one-to-many. (If you’re still having trouble with these data
modeling concepts, don’t struggle too hard just now—the source code
and schema coming up shortly will hopefully help you see what is
happening here.)
OK, back to considering this new composite-element
definition as a whole.
It specifies that we want to use a new
AlbumTrack
class as the values that appear in
our Album
data bean’s tracks list. The body of the composite-element
tag defines the
properties of AlbumTrack
, which group all the
information we need about a track on an album. The syntax for these
nested properties is no different than that of the outer mappings
for Album
’s own properties. They can even
include their own nested composite elements, collections, or (as
seen here) meta
elements. This
gives us tremendous flexibility to set up fine-grained mappings
that
retain a healthy degree of object-oriented encapsulation.
Inside our composite AlbumTrack
mapping, we are recording an association with the actual
Track
(the many-to-one
element we just examined) to
be played at each position within the
Album
.
The composite mapping also keeps track of the disc number on which that track is found.
And finally it stores this entry’s position on that disc (for example, track 3 of disc 2).
This mapping achieves the goals with which we started with illustrates how arbitrary information can be attached to a collection of associations.
The source for the component class itself can be found in Example 5-6, and it might help clarify this discussion. Compare this source code with its graphical representation at the bottom of Figure 5-1.
You may have noticed that we chose an explicit column name
of TRACK_ID
to use for
the many-to-one
link to the TRACK
table. We’ve actually been doing this in
a number of places, but previously it didn’t require an entire separate
line. It’s worth talking about the reasoning behind this choice. Without
this instruction, Hibernate will just use the property name (track) for the column name. You can use any
names you want for your columns, but Java
Database Best Practices encourages naming foreign key columns
the same as the primary keys in the original tables to which they refer.
This helps data modeling tools recognize and display the “natural joins”
the foreign keys represent, which makes it easier for people to
understand and work with the data. This consideration is also why I
included the table names as part of the primary keys’ column
names.
I was all set to explain that by choosing to use a composite
element to encapsulate our augmented track list, we’d have to write the
Java source for AlbumTrack
ourselves. I was sure
this went far beyond the capabilities of the code generation tool. Much
to my delight, when I tried ant
codegen to see what sort of errors would result, the command
reported success, and both Album.java and AlbumTrack.java appeared in the source
directory!
Sometimes it’s nice to be proved wrong.
It was at this point that I went back and added the use-in-tostring
meta element for the track’s many-to-one mapping inside the
component. I wasn’t sure this would work either, because the only
examples of its use I’d found in the reference manual were attached to
actual property
tags. But work it
did, exactly as I had hoped.
The Hibernate best practices encourage using fine-grained classes and mapping them as components. Given how easily the code generation tool allows you to create them from your mapping documents, there is absolutely no excuse for ignoring this advice. Example 5-6 shows the source generated for our nested composite mapping.
package com.oreilly.hh.data; // Generated Jun 21, 2007 11:11:48 AM by Hibernate Tools 3.2.0.b9 /** * Represents an album in the music database, an organized list of tracks. * @author Jim Elliott (with help from Hibernate) */ public class AlbumTrack implements java.io.Serializable { private Track track; private Integer disc; private Integer positionOnDisc; public AlbumTrack() { } public AlbumTrack(Track track, Integer disc, Integer positionOnDisc) { this.track = track; this.disc = disc; this.positionOnDisc = positionOnDisc; } public Track getTrack() { return this.track; } public void setTrack(Track track) { this.track = track; } public Integer getDisc() { return this.disc; } public void setDisc(Integer disc) { this.disc = disc; } public Integer getPositionOnDisc() { return this.positionOnDisc; } public void setPositionOnDisc(Integer positionOnDisc) { this.positionOnDisc = positionOnDisc; } /** * toString * @return String */ public String toString() { StringBuffer buffer = new StringBuffer(); buffer.append(getClass().getName()).append("@").append( Integer.toHexString(hashCode())).append(" ["); buffer.append("track").append("='").append(getTrack()).append("' "); buffer.append("]"); return buffer.toString(); } }
This looks similar to the generated code for entities we’ve seen
in previous chapters, but it lacks an id
property, which makes
sense. Component classes don’t need identifier fields, and they need not
implement any special interfaces. The class JavaDoc is shared with the
Album
class, in which this component is used. The
source of the Album
class itself is a typical
generated entity, so there’s no need to reproduce it here.
At this point we can build the schema for these new mappings, via ant schema. Example 5-7 shows highlights of the resulting schema creation process. This is the concrete HSQLDB representation of the schema modeled at the top of Figure 5-1.
... [hibernatetool] create table ALBUM (ALBUM_ID integer generated by default as identity (start with 1), TITLE varchar(255) not null, numDiscs integer, added date, primary key (ALBUM_ID)); ... [hibernatetool] create table ALBUM_ARTISTS (ALBUM_ID integer not null, ARTIST_ID integer not null, primary key (ALBUM_ID, ARTIST_ID)); ... [hibernatetool] create table ALBUM_COMMENTS (ALBUM_ID integer not null, COMMENT varchar(255)); ... [hibernatetool] create table ALBUM_TRACKS (ALBUM_ID integer not null, TRACK_ID integer, disc integer, positionOnDisc integer, LIST_POS integer not null, primary key (ALBUM_ID, LIST_POS)); ... [hibernatetool] create index ALBUM_TITLE on ALBUM (TITLE); ... [hibernatetool] alter table ALBUM_ARTISTS add constraint FK7BA403FC620962DF foreign key (ARTIST_ID) references ARTIST; [hibernatetool] alter table ALBUM_ARTISTS add constraint FK7BA403FC3C553835 foreign key (ALBUM_ID) references ALBUM; [hibernatetool] alter table ALBUM_COMMENTS add constraint FK1E2C21E43C553835 foreign key (ALBUM_ID) references ALBUM; [hibernatetool] alter table ALBUM_TRACKS add constraint FKD1CBBC782DCBFAB5 foreign key (TRACK_ID) references TRACK; [hibernatetool] alter table ALBUM_TRACKS add constraint FKD1CBBC783C553835 foreign key (ALBUM_ID) references ALBUM; ...
You may find that making radical changes to the schema causes problems for Hibernate or the HSQLDB driver. When I switched to this new approach for mapping album tracks, I ran into trouble because the first set of mappings established database constraints that Hibernate didn’t know to drop before trying to build the revised schema. This prevented it from dropping and recreating some tables. If this ever happens to you, you can delete the database file (music.script in the data directory) and start from scratch, which should work fine. Recent versions of Hibernate also seem more robust in scenarios like this.
Figure 5-2 shows our enriched schema in HSQLDB’s graphical management interface.
You might wonder why we use the separate
Track
class at all, rather than simply embedding
all that information directly in our enhanced
AlbumTrack
collection. The simple answer is that
not all tracks are part of an album—some might be singles, downloads, or
otherwise independent. Given that we need a separate table to keep track
of these anyway, it would be a poor design choice to duplicate its
contents in the AlbumTracks
table rather than
associating with it. There is also a more subtle advantage to this
approach, which is actually used in my own music database: this
structure allows us to share a single track file between multiple
albums. If the exact same recording appears on an album, a “best of”
collection, and one or more period collections or sound tracks, linking
all these albums to the same track file saves disk space.
Another point worth noting about the ALBUM_TRACK
schema is that there is no obvious
ID
column. If you look back at the
schema definition Hibernate emitted for
ALBUM_TRACK
in Example 5-7, you’ll see the phrase primary key
(ALBUM_ID, LIST_POS)
. Hibernate has noticed that, given the
relationships we’ve requested in Album.hbm.xml, a row in the ALBUM_TRACK
table can be uniquely identified
by a combination of the ID of the Album
with
which it’s associated and the index within the list it’s modeling, so it
has set these up as a composite key
for the table. This is a nice little optimization we didn’t even have to
think about. Also notice that one of those columns is a property of the
AlbumTrack
class while the other is not. We’ll
look at a slightly different way to model this relationship in Chapter 7.
Let’s look at some sample code showing how to use these new data
objects. Example 5-8 shows
a class that creates an album record and its list of tracks, then prints
it out to test the debugging support that we’ve configured through the
toString()
method.
package com.oreilly.hh; import org.hibernate.*; import org.hibernate.cfg.Configuration; import com.oreilly.hh.data.*; import java.sql.Time; import java.util.*; /** * Create sample album data, letting Hibernate persist it for us. */ public class AlbumTest { /** * Quick and dirty helper method to handle repetitive portion of creating * album tracks. A real implementation would have much more flexibility. */ private static void addAlbumTrack(Album album, String title, String file, Time length, Artist artist, int disc, int positionOnDisc, Session session) { Track track = new Track(title, file, length, new HashSet<Artist>(), new Date(), (short)0, new HashSet<String>()); track.getArtists().add(artist); session.save(track); album.getTracks().add(new AlbumTrack(track, disc, positionOnDisc)); } public static void main(String args[]) throws Exception { // Create a configuration based on the properties file we've put // in the standard place. Configuration config = new Configuration(); config.configure(); // Get the session factory we can use for persistence SessionFactory sessionFactory = config.buildSessionFactory(); // Ask for a session using the JDBC information we've configured Session session = sessionFactory.openSession(); Transaction tx = null; try { // Create some data and persist it tx = session.beginTransaction(); Artist artist = CreateTest.getArtist("Martin L. Gore", true, session); Album album = new Album("Counterfeit e.p.", 1, new HashSet<Artist>(), new HashSet<String>(), new ArrayList<AlbumTrack>(5), new Date()); album.getArtists().add(artist); session.save(album); addAlbumTrack(album, "Compulsion", "vol1/album83/track01.mp3", Time.valueOf("00:05:29"), artist, 1, 1, session); addAlbumTrack(album, "In a Manner of Speaking", "vol1/album83/track02.mp3", Time.valueOf("00:04:21"), artist, 1, 2, session); addAlbumTrack(album, "Smile in the Crowd", "vol1/album83/track03.mp3", Time.valueOf("00:05:06"), artist, 1, 3, session); addAlbumTrack(album, "Gone", "vol1/album83/track04.mp3", Time.valueOf("00:03:32"), artist, 1, 4, session); addAlbumTrack(album, "Never Turn Your Back on Mother Earth", "vol1/album83/track05.mp3", Time.valueOf("00:03:07"), artist, 1, 5, session); addAlbumTrack(album, "Motherless Child", "vol1/album83/track06.mp3", Time.valueOf("00:03:32"), artist, 1, 6, session); System.out.println(album); // We're done; make our changes permanent tx.commit(); // This commented out section is for experimenting with deletions. //tx = session.beginTransaction(); //album.getTracks().remove(1); //session.update(album); //tx.commit(); //tx = session.beginTransaction(); //session.delete(album); //tx.commit(); } catch (Exception e) { if (tx != null) { // Something went wrong; discard all partial changes tx.rollback(); } throw new Exception("Transaction failed", e); } finally { // No matter what, close the session session.close(); } // Clean up after ourselves sessionFactory.close(); } }
In this simple example we’re creating an album with just one disc. This quick-and-dirty method can’t cope with many variations, but it does allow the example to be compressed nicely.
We also need a new target at the end of build.xml to invoke the class. Add the lines
of Example 5-9 at the
end of the file (but inside the project
element, of course).
<target name="atest" description="Creates and persists some album data" depends="compile"> <java classname="com.oreilly.hh.AlbumTest" fork="true"> <classpath refid="project.class.path"/> </java> </target>
With this in place, assuming you’ve generated the schema, run
ant ctest followed by ant atest. (Running ctest
first is optional, but having some
extra data in there to begin with makes the album data somewhat more
interesting. Recall that you can run these targets in one command as
ant ctest atest, and if you want to
start by erasing the contents of the database first, you can invoke
ant schema ctest atest.) The
debugging output produced by this command is shown in Example 5-10. Although
admittedly cryptic, you should be able to see that the album and tracks
have been created, and the order of the tracks has been
maintained.
atest: [java] com.oreilly.hh.data.Album@5bcf3a [title='Counterfeit e.p.' tracks='[ com.oreilly.hh.data.AlbumTrack@6a346a [track='com.oreilly.hh.data.Track@973271 [ title='Compulsion' volume='Volume[left=100, right=100]' sourceMedia='CD' ]' ], c om.oreilly.hh.data.AlbumTrack@8e0e1 [track='com.oreilly.hh.data.Track@e3f8b9 [ti tle='In a Manner of Speaking' volume='Volume[left=100, right=100]' sourceMedia=' CD' ]' ], com.oreilly.hh.data.AlbumTrack@de59f0 [track='com.oreilly.hh.data.Trac k@e2d159 [title='Smile in the Crowd' volume='Volume[left=100, right=100]' source Media='CD' ]' ], com.oreilly.hh.data.AlbumTrack@1e5a36 [track='com.oreilly.hh.da ta.Track@b4bb65 [title='Gone' volume='Volume[left=100, right=100]' sourceMedia=' CD' ]' ], com.oreilly.hh.data.AlbumTrack@7b1683 [track='com.oreilly.hh.data.Trac k@3171e [title='Never Turn Your Back on Mother Earth' volume='Volume[left=100, r ight=100]' sourceMedia='CD' ]' ], com.oreilly.hh.data.AlbumTrack@e2e4d7 [track=' com.oreilly.hh.data.Track@1dfc6e [title='Motherless Child' volume='Volume[left=1 00, right=100]' sourceMedia='CD' ]' ]]' ]
If we run our old query test, we can see both the old and new data, as in Example 5-11.
% ant qtest
Buildfile: build.xml
...
qtest:
[java] Track: "Russian Trance" (PPK) 00:03:30
[java] Track: "Video Killed the Radio Star" (The Buggles) 00:03:49
[java] Track: "Gravity's Angel" (Laurie Anderson) 00:06:06
[java] Track: "Adagio for Strings (Ferry Corsten Remix)" (Ferry Corsten, Sa
muel Barber, William Orbit) 00:06:35
[java] Track: "Test Tone 1" 00:00:10
[java] Comment: Pink noise to test equalization
[java] Track: "Compulsion" (Martin L. Gore) 00:05:29
[java] Track: "In a Manner of Speaking" (Martin L. Gore) 00:04:21
[java] Track: "Smile in the Crowd" (Martin L. Gore) 00:05:06
[java] Track: "Gone" (Martin L. Gore) 00:03:32
[java] Track: "Never Turn Your Back on Mother Earth" (Martin L. Gore) 00:03
:07
[java] Track: "Motherless Child" (Martin L. Gore) 00:03:32
BUILD SUCCESSFUL
Total time: 2 seconds
Finally, Figure 5-3 shows a query in the HSQLDB interface that
examines the contents of the ALBUM_TRACKS
table.
Hibernate is completely responsible for managing the ALBUM_TRACKS
table,
adding and deleting rows (and, if necessary, renumbering LIST_POS
values) as entries are added to or
removed from Album
beans’ tracks properties. You can test this by writing
a test program to delete the second track from our test album and see the
result. A very quick-and-dirty way to do this would be to add the
following four lines (shown in Example 5-12) right after the existing
tx.commit()
line in Example 5-8, and then run ant
schema ctest atest db.
tx = session.beginTransaction(); album.getTracks().remove(1); session.update(album); tx.commit();
Doing so changes the contents of ALBUM_TRACKS
as shown in Figure 5-4 (compare
this with the original contents in Figure 5-3). The second record has been removed
(remember that Java list elements are indexed starting with zero), and
LIST_POS
has been adjusted so that it
retains its consecutive nature, corresponding to the indices of the list
elements (the values you’d use when calling
tracks.get()
).
This happens because Hibernate understands that this list is “owned”
by the Album
record, and that the lifecycles of the
two objects are intimately connected. This notion of lifecycle becomes
more clear if you consider what happens if the entire
Album
is deleted: all of the associated records in
ALBUM_TRACKS
will be deleted as well.
(Go ahead and modify the test program to try this if you’re not
convinced.)
Contrast this with the relationship between the ALBUM
table and the TRACK
table. Tracks are sometimes associated
with albums, but they are sometimes independent. Removing a track from the
list got rid of a row in ALBUM_TRACKS
,
eliminating the link between the album and track, but didn’t get rid of
the row in TRACK
, so it didn’t delete
the persistent Track
object itself. Similarly,
deleting the Album
would eliminate all the
associations in the collection, but none of the actual
Track
s. It’s the responsibility of our code to take
care of that when appropriate (probably after consulting the user, in case
any of the track records might be shared across multiple albums, as
discussed earlier).
If we don’t need the flexibility of sharing the same track between
albums—disk space is pretty cheap lately given the size of compressed
audio—we can let Hibernate manage the TRACK
records for the album in the same way it
does the ALBUM_TRACKS
collection. It
won’t assume it should do this, because Track
and
Album
objects can exist independently, but we can
establish a lifecycle relationship between them in the album mapping
document.
By now you’re probably not surprised that there’s a way to automate this.
Example 5-13 shows (in bold) the changes we’d make to the tracks property mapping in Album.hbm.xml.
<list name="tracks" table="ALBUM_TRACKS"cascade="all"
> <meta attribute="use-in-tostring">true</meta> <key column="ALBUM_ID" /> <index column="LIST_POS" /> <composite-element class="com.oreilly.hh.AlbumTrack"> <many-to-one class="com.oreilly.hh.Track" name="track"cascade="all"
> <meta attribute="use-in-tostring">true</meta> <column name="TRACK_ID" /> </many-to-one> <property name="disc" type="integer" /> <property name="positionOnDisc" type="integer" /> </composite-element> </list>
The cascade
attribute tells Hibernate that you want operations performed on a
“parent” object to be transitively applied to its “child” or “dependent”
objects. It’s applicable to all forms of collections and associations.
There are several possible values from which to choose. The most common
are none
(the default), save-update
, delete
, and all
(which combines save-update
and delete
). You can also change the default from
none
to save-update
throughout your entire
mapping document by supplying a default-cascade
attribute in the hibernate-mapping
tag itself.
In our example, we want the tracks owned by an album to be
automatically managed by the album, so that when we delete the album,
its tracks are deleted. Note that we need to apply the cascade
attribute both to the tracks
collection and its constituent
track
element to achieve this. Also,
by using a cascade
value of
all
, we eliminate the need to
explicitly save any Track
objects we create for
the album—the addAlbumTrack()
method of Example 5-8 no longer needs the
line:
session.save(track);
By telling Hibernate that it’s fully responsible for the relationship between an album and its track, we enable it to persist tracks when they’re added to the album as well as delete them when the album itself is deleted.
Delegating this sort of bookkeeping to the mapping layer can be
very convenient, freeing you to focus on more abstract and important
tasks, so it is worth using when appropriate. It’s reminiscent of the
liberation provided by Java’s pervasive garbage collection, but it can’t
be as comprehensive because there is no definitive way to know when
you’re finished with persistent data by performing reachability
analysis; you need to indicate it by calling
delete()
and establishing lifecycle
connections. The trade-off between flexibility and simple automation is
yours to make, based on the nature of your data and the needs of your
project.
Hibernate’s management of lifecycle relationships is not
fool-proof—or perhaps it’s more accurate to say it’s not
all-encompassing. For example, if you use the
Collections
methods to remove a
Track
from an Album
’s
tracks
property, this breaks the
link between the Album
and
Track
but does not actually delete the
Track
record. Even if you later delete the
entire Album
, this Track
will remain, because it wasn’t linked to the
Album
at the time it was deleted. Try some of
these experiments by modifying AlbumTest.java appropriately and look at
the resulting data in the tables!
And, actually, there are certain special cases where Hibernate
can take care of even this level of detail. As long as you are using a
many-to-one mapping in a parent-child relationship, you can mark it
with a cascade
value of delete-orphan
. More details about this sort
of thing can be found in the Transitive
persistence section of the online
reference manual.
It’s also possible for objects and tables to have associations back to themselves. This supports persistent recursive data structures like trees, in which nodes link to other nodes. Tracing through a database table storing such relationships using a SQL query interface is a major chore. Luckily, once it’s mapped to Java objects, the process is much more readable and natural.
One way we might use a reflexive link in our music database is to allow alternate names for artists. This is useful more often than you might expect, because it makes it very easy to let the user find either “The Smiths” or “Smiths, The” depending on how they’re thinking of the group, with little code, and in a language-independent way.
I mean human language here—English versus Spanish or something else. Put the links in the data rather than trying to write tricky code to guess when an artist name should be permuted.
All you need to do is add another field to the
Artist
mapping in Artist.hbm.xml, establishing a link back
to Artist
. Example 5-14 shows
one option.
<many-to-one name="actualArtist" class="com.oreilly.hh.data.Artist"> <meta attribute="use-in-tostring">true</meta> </many-to-one>
This gives us an actualArtist
property that we can set to the id of the “definitive”
Artist
record when we’re setting up an alternate
name. For example, our “The Smiths” record might have id 5
, and
its actualArtist
field would
be null
since it is definitive. Then
we can create an “alias” Artist
record with the
name “Smiths, The” at any time, and set the actualArtist field in that record to point to
record 5
.
This kind of reflexive link is one instance where a column
containing a foreign key can’t be named the same as the key column to
which it is a link. We are associating a row in ARTIST
with another row in ARTIST
, and of course the table already has
a column named ARTIST_ID
.
Why is this association set up as many-to-one? There might be
many alias records that point to one particular definitive
Artist
. So, each nickname needs to store the
id of the actual artist record for
which it is an alternative name. This is, in the language of data
modeling, a many-to-one relationship.
Code that looks up artists just needs to check the actualArtist
property
before returning. If it’s null
, all
is well. Otherwise, it should return the record indicated by actualArtist. Example 5-15 shows how
we could extend the getArtist()
method in
CreateTest
to support this new feature (additions
are in bold). Notice that the Artist
constructor
gets a new argument for setting actualArtist
, which
means we had to update the other places that call it in
CreateTest
too, even though we aren’t showing
them here.
public static Artist getArtist(String name, boolean create, Session session) { Query query = session.getNamedQuery("com.oreilly.hh.artistByName"); query.setString("name", name); Artist found = (Artist)query.uniqueResult(); if (found == null && create) { found = new Artist(name, new HashSet(), null
); session.save(found); }if (found != null && found.getActualArtist() != null) {
return found.getActualArtist();
}
return found; }
Hopefully this chapter has given you a feel for the rich and powerful ways you can use associations and collections in Hibernate. As should be obvious from the way you can nest and combine these capabilities, there are far more variations than we can hope to cover in a book like this.
The good news is that Hibernate seems well equipped to handle almost any kind of relationship your application might need, and it can even do the drudge work of building the data classes and database schema for you. This works much more effectively and deeply than I ever expected it would when I started creating these examples.
[4] The order-by
attribute
and SQL sorting of collections is only available if you’re using
version 1.4 or later of the Java SDK, since it relies on the
LinkedHashSet
or
LinkedHashMap
classes introduced in that
release.