Wednesday, October 21, 2009

Section 5.3. Augmenting Associations in Collections








5.3. Augmenting Associations in Collections


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.



5.3.1. How do I do that?


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.


NOTE


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.


Example 5-4. Album.hbm.xml, the mapping definition for an album



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


Example 5-5. Adding Album.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"/>
<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).



Figure 5-1. Models of the tables, concepts, and objects involved in
representing album tracks








All right, armed with this reminder and elaboration of the
conceptual framework, we're ready to look at the details of Example 5-4:



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.




5.3.2. What just happened?


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!


NOTE


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.



Example 5-6. Code generated for AlbumTrack.java

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.


Example 5-7. Additions to the schema caused by our new album mapping



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



Figure 5-2. The schema with album-related tables








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.


Example 5-8. Source of AlbumTest.java



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


Example 5-9. New target to run our album test class



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


Example 5-10. Output from running the album test



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.


Example 5-11. All tracks less than seven minutes long, whether from albums or
otherwise



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



Figure 5-3. Our augmented collection of associations in action










5.3.3. What about…


…deleting, rearranging, and otherwise manipulating these
interrelated pieces of information? This is actually supported
fairly automatically, as the next section
illustrates.










No comments: