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 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 until 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 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 multi-disc albums.

NOTE

This flexibility took 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
 1    <?xml version="1.0"?>  2    <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 2.0//EN"  3              "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">  4  5    <hibernate-mapping>  6      <class name="com.oreilly.hh.Album" table="ALBUM">  7        <meta attribute="class-description">  8          Represents an album in the music database, an organized list of tracks.  9          @author Jim Elliott (with help from Hibernate) 10        </meta> 11 12        <id name="id" type="int" column="ALBUM_ID"> 13          <meta attribute="scope-set">protected</meta> 14          <generator class="native"/> 15        </id> 16 17        <property name="title" type="string"> 18          <meta attribute="use-in-tostring">true</meta> 19          <column name="TITLE" not-null="true" index="ALBUM_TITLE"/> 20        </property> 21 22        <property name="numDiscs" type="integer"/> 23 24        <set name="artists" table="ALBUM_ARTISTS"> 25          <key column="ALBUM_ID"/> 26          <many-to-many class="com.oreilly.hh.Artist" column="ARTIST_ID"/> 27        </set> 28 29        <set name="comments" table="ALBUM_COMMENTS"> 30          <key column="ALBUM_ID"/> 31          <element column="COMMENT" type="string"/> 32        </set> 33 34        <list name="tracks" table="ALBUM_TRACKS"> 35         <meta attribute="use-in-tostring">true</meta> 36          <key column="ALBUM_ID"/> 37          <index column="POSITION"/> 38          <composite-element class="com.oreilly.hh.AlbumTrack"> 39            <many-to-one name="track" class="com.oreilly.hh.Track"> 40              <meta attribute="use-in-tostring">true</meta> 41              <column name="TRACK_ID"/> 42            </many-to-one> 43            <property name="disc" type="integer"/> 44            <property name="positionOnDisc" type="integer"/> 45          </composite-element> 46         </list> 47 48         <property name="added" type="date"> 49           <meta attribute="field-description">When the album was created</meta> 50         </property> 51 52       </class> 53    </hibernate-mapping> 

A lot of this is similar to mappings we've seen before, but the tracks list (starting on line 34) 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 simpler). 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
figs/hibernate_0501.jpg

If you compare lines 34-46 of Example 5-4 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, lines 38-45. 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 on line 39 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.)

Okay, back to this new composite-element definition. 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, lines 39-44, 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 attributes. This gives us tremendous flexibility to set up fine-grained mappings that retain a healthy degree of object-oriented encapsulation.

In our composite AlbumTrack mapping, we are recording an association with the actual Track (lines 39-42) to be played at each position within the Album , as well as the disc on which that track is found (line 43), and, on line 44, this entry's position on that disc (for example, track 3 of disc 2). This achieves the goals we started with and illustrates how arbitrary information can be attached to a collection of associations. The source for the class itself can be found in Example 5-5, 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 I chose an explicit column name of TRACK_ID to use for the many-to-one link to the TRACK table (line 41). I'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 attribute for the track many-to-one mapping inside the component. I wasn't sure this would work either, because the only examples of its use I've found in the reference manual are attached to actual property tags. But work it did, exactly as I 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-5 shows the source generated for our nested composite mapping.

Example 5-5. Code generated for AlbumTrack.java
 package com.oreilly.hh; import java.io.Serializable; import org.apache.commons.lang.builder.ToStringBuilder; /**  *       Represents an album in the music database, an organized list of tracks.  *       @author Jim Elliott (with help from Hibernate)  * */ public class AlbumTrack implements Serializable {     /** nullable persistent field */     private int disc;          /** nullable persistent field */     private int positionOnDisc;          /** nullable persistent field */     private com.oreilly.hh.Track track;          /** full constructor */     public AlbumTrack(int disc, int positionOnDisc, com.oreilly.hh.Track track) {         this.disc = disc;         this.positionOnDisc = positionOnDisc;         this.track = track;     }          /** default constructor */     public AlbumTrack() {     }          public int getDisc() {         return this.disc;     }          public void setDisc(int disc) {         this.disc = disc;     }          public int getPositionOnDisc() {         return this.positionOnDisc;     }          public void setPositionOnDisc(int positionOnDisc) {         this.positionOnDisc = positionOnDisc;     }          public com.oreilly.hh.Track getTrack() {         return this.track;     }          public void setTrack(com.oreilly.hh.Track track) {         this.track = track;     }          public String toString() {         return new ToStringBuilder(this)             .append("track", getTrack())             .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-6 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-6. Additions to the schema caused by our new Album mapping
 [schemaexport] create table ALBUM ( [schemaexport]    ALBUM_ID INTEGER NOT NULL IDENTITY, [schemaexport]    TITLE VARCHAR(255) not null, [schemaexport]    numDiscs INTEGER, [schemaexport]    added DATE [schemaexport] ) ... [schemaexport] create table ALBUM_COMMENTS ( [schemaexport]    ALBUM_ID INTEGER not null, [schemaexport]    COMMENT VARCHAR(255) [schemaexport] ) ... [schemaexport] create table ALBUM_ARTISTS ( [schemaexport]    ALBUM_ID INTEGER not null, [schemaexport]    ARTIST_ID INTEGER not null, [schemaexport]    primary key (ALBUM, ARTIST) [schemaexport] ) ... [schemaexport] create table ALBUM_TRACKS ( [schemaexport]    ALBUM_ID INTEGER not null, [schemaexport]    TRACK_ID INTEGER, [schemaexport]    disc INTEGER, [schemaexport]    positionOnDisc INTEGER, [schemaexport]    POSITION INTEGER not null, [schemaexport]    primary key (ALBUM_ID, POSITION) [schemaexport] ) ... [schemaexport] create index ALBUM_TITLE on ALBUM (title) ... [schemaexport] alter table ALBUM_COMMENTS add constraint FK1E2C21E43B7864F foreign key (ALBUM_ID) references ALBUM ... [schemaexport] alter table ALBUM_ARTISTS add constraint FK7BA403FC3B7864F foreign key (ALBUM_ID) references ALBUM ... [schemaexport] alter table ALBUM_TRACKS add constraint FKD1CBBC783B7864F foreign key (ALBUM_ID) references ALBUM ... [schemaexport] alter table ALBUM_TRACKS add constraint FKD1CBBC78697F14B foreign key (TRACK_ID) references TRACK 

You may find that making radical changes to the schema causes problems for Hibernate or the HSQLDB driver. When I switched between the above two approaches 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.


Figure 5-2 shows our enriched schema in HSQLDB's graphical management interface.

Figure 5-2. The schema with album- related tables
figs/hibernate_0502.jpg

You might wonder why we use the separate Track class at all, rather than simply embedding all that information directly in our enhanced AlbumTracks 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 same song 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.

Let's look at some sample code showing how to use these new data objects. Example 5-7 shows a class that creates an album record and its list of tracks, then prints it out to test the debugging; support we've configured for the toString() method.

Example 5-7. Source of AlbumTest.java
 1    package com.oreilly.hh;  2  3    import net.sf.hibernate.*;  4    import net.sf.hibernate.cfg.Configuration;  5  6    import java.sql.Time;  7    import java.util.*;  8  9    /** 10    * Create sample album data, letting Hibernate persist it for us. 11    */ 12    public class AlbumTest { 13 14        /** 15         * Quick and dirty helper method to handle repetitive portion of creating 16         * album tracks. A real implementation would have much more flexibility. 17         */ 18        private static void addAlbumTrack(Album album, String title, String file, 19                                          Time length, Artist artist, int disc, 20                                          int positionOnDisc, Session session) 21            throws HibernateException 22        { 23            Track track = new Track(title, file, length, new Date(), (short)0, 24                                    new HashSet(), new HashSet()); 25            track.getArtists().add(artist); 26            session.save(track); 27            album.getTracks().add(new AlbumTrack(disc, positionOnDisc, track)); 28        } 29 30        public static void main(String args[]) throws Exception { 31            // Create a configuration based on the properties file we've put 32            // in the standard place. 33            Configuration config = new Configuration(); 34 35            // Tell it about the classes we want mapped. 36            config.addClass(Track.class).addClass(Artist.class); 37            config.addClass(Album.class); 38 39            // Get the session factory we can use for persistence 40            SessionFactory sessionFactory = config.buildSessionFactory(); 41 42            // Ask for a session using the JDBC information we've configured 43            Session session = sessionFactory.openSession(); 44            Transaction tx = null; 45            try { 46                // Create some data and persist it 47                tx = session.beginTransaction(); 48 49                Artist artist = CreateTest.getArtist("Martin L. Gore", true, 50                                                     session); 51                List albumTracks = new ArrayList(5); 52                Album album = new Album("Counterfeit e.p.", 1, new Date(), 53                                      albumTracks, new HashSet(), new HashSet()); 54                album.getArtists().add(artist); 55                session.save(album); 56 57                addAlbumTrack(album, "Compulsion", "vol1/album83/track01.mp3", 58                             Time.valueOf("00:05:29"), artist, 1, 1, session); 59                addAlbumTrack(album, "In a Manner of Speaking", 60                             "vol1/album83/track02.mp3", Time.valueOf("00:04:21"), 61                             artist, 1, 2, session); 62                addAlbumTrack(album, "Smile in the Crowd", 63                             "vol1/album83/track03.mp3", Time.valueOf("00:05:06"), 64                             artist, 1, 3, session); 65                addAlbumTrack(album, "Gone", "vol1/album83/track04.mp3", 66                             Time.valueOf("00:03:32"), artist, 1, 4, session); 67                addAlbumTrack(album, "Never Turn Your Back on Mother Earth", 68                             "vol1/album83/track05.mp3", Time.valueOf("00:03:07"), 69                             artist, 1, 5, session); 70                addAlbumTrack(album, "Motherless Child", "vol1/album83/track06.mp3", 71                               Time.valueOf("00:03:32"), artist, 1, 6, session); 72 73                System.out.println(album); 74 75                // We're done; make our changes permanent 76                tx.commit(); 77 78            } catch (Exception e) { 79                if (tx != null) { 80                    // Something went wrong; discard all partial changes 81                    tx.rollback(); 82                } 83                throw e; 84            } finally { 85                // No matter what, close the session 86                session.close(); 87            } 88 89            // Clean up after ourselves 90            sessionFactory.close(); 91        } 92    } 

The addAlbumTrack() method starting on line 14 creates and persists a Track object given the specified parameters, associates it with a single Artist (line 25), then adds it to the supplied Album , recording the disc it's on and its position within that disc (line 27). 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-8 at the end of the file (but inside the project tag, of course).

Example 5-8. 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-9. 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-9. Output from running the album test
 atest:      [java] com.oreilly.hh.Album@863cc1[id=0,title=Counterfeit e.p.,tracks=[com. oreilly.hh.AlbumTrack@b3cc96[track=com.oreilly.hh. Track@fea539[id=7,title=Compulsion]], com.oreilly.hh.AlbumTrack@3ca972[track=com. oreilly.hh.Track@f2e328[id=8,title=In a Manner of Speaking]], com.oreilly.hh. AlbumTrack@98a1f4[track=com.oreilly.hh.Track@1f6c18[id=9,title=Smile in the Crowd]], com.oreilly.hh.AlbumTrack@b0d990[track=com.oreilly.hh. Track@f1cdfb[id=10,title=Gone]], com.oreilly.hh.AlbumTrack@9baf0b[track=com. oreilly.hh.Track@a59d2[id=11,title=Never Turn Your Back on Mother Earth]], com. oreilly.hh.AlbumTrack@10c69[track=com.oreilly.hh. Track@8f1ed7[id=12,title=Motherless Child]]]] 

If we run our old query test, we can see both the old and new data, as in Example 5-10.

Example 5-10. All tracks are 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, William Orbit, Samuel Barber) 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: 12 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
figs/hibernate_0503.jpg



Hibernate. A Developer's Notebook
Hibernate: A Developers Notebook
ISBN: 0596006969
EAN: 2147483647
Year: 2003
Pages: 65
Authors: James Elliott

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net