Wednesday 26 September 2012

Collections and Associations

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


NOTE


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


The information we need to keep track of 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 someone we 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.)

Example 4-1. Mapping document for the Artist class

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

7 <class name="com.oreilly.hh.Artist" table="ARTIST">

8 <meta attribute="class-description">

9 Represents an artist who is associated with a track or album.

10 @author Jim Elliott (with help from Hibernate)

11 </meta>

12

13 <id name="id" type="int" column="ARTIST_ID">

14 <meta attribute="scope-set">protected</meta>

15 <generator class="native"/>

16 </id>

17

18 <property name="name" type="string">

19 <meta attribute="use-in-tostring">true</meta>

20 <column name="NAME" not-null="true" unique="true" index="ARTIST_NAME"/>

21 </property>

22

23 <set name="tracks" table="TRACK_ARTISTS" inverse="true">

24 <meta attribute="field-description">Tracks by this artist</meta>

25 <key column="ARTIST_ID"/>

26 <many-to-many class="com.oreilly.hh.Track" column="TRACK_ID"/>

27 </set>

28

29 </class>

30

31 </hibernate-mapping>




Our mapping for the name property on lines 18-21 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 show the artist's name as well as the cryptic synthetic ID when it is printed, as an aid for debugging (you can see the result near the bottom of Example 4-3). 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.

Notice that we can represent the fact that an artist is associated with one or more tracks quite naturally in this file (lines 23-27). This 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 in the discussion of Example 4-2, 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. [4.1]

[4.1] 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 one, and you can even view this chapter online at www.oreilly.com/catalog/javadtabp/chapter/ch02.pdf.

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

As seen on line 24, 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!

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

Example 4-2. 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.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 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. 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, as long as we mark one of the directions as inverse. That's why we did so on line 23 of Example 4-1.

While we're updating the Track mapping document we might as well beef up the title property along the lines of what we did for 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. This time Hibernate reports that two files are processed, as expected. If you look at Track.java you'll see the new Set-valued property artists has been added, and toString() has been enhanced. Example 4-3 shows the content of the new Artist.java.

Example 4-3. Code generated for the Artist class

package com.oreilly.hh;



import java.io.Serializable;

import java.util.Set;

import org.apache.commons.lang.builder.EqualsBuilder;

import org.apache.commons.lang.builder.HashCodeBuilder;

import org.apache.commons.lang.builder.ToStringBuilder;



/**

* Represents an artist who is associated with a track or album.

* @author Jim Elliott (with help from Hibernate)

*

*/

public class Artist implements Serializable {



/** identifier field */

private Integer id;



/** nullable persistent field */

private String name;



/** persistent field */

private Set tracks;



/** full constructor */

public Artist(String name, Set tracks) {

this.name = name;

this.tracks = tracks;

}



/** default constructor */

public Artist() {

}



/** minimal constructor */

public Artist(Set tracks) {

this.tracks = tracks;

}



public Integer getId() {

return this.id;

}



protected void setId(Integer 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;

}



public String toString() {

return new ToStringBuilder(this)

.append("id", getId())

.append("name", getName())

.toString();

}



public boolean equals(Object other) {

if ( !(other instanceof Artist) ) return false;

Artist castOther = (Artist) other;

return new EqualsBuilder()

.append(this.getId(), castOther.getId())

.isEquals();

}



public int hashCode() {

return new HashCodeBuilder()

.append(getId())

.toHashCode();

}

}




Once the classes are created, we can use ant schema to build the new database schema that supports them.

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 stack traces 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 be something that will improve in future versions (of Hibernate or HSQLDB), or it may just be a wart we learn to live with.






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-4 shows highlights.

Example 4-4. Excerpts from our new schema generation

[schemaexport] create table TRACK_ARTISTS (

[schemaexport] ARTIST_ID INTEGER not null,

[schemaexport] TRACK_ID INTEGER not null,

[schemaexport] primary key (TRACK_ID, ARTIST_ID)

[schemaexport] )

...

[schemaexport] create table ARTIST (

[schemaexport] ARTSIT_ID INTEGER NOT NULL IDENTITY,

[schemaexport] name VARCHAR(255) not null

[schemaexport] unique (name)

[schemaexport] )

...

[schemaexport] create table TRACK (

[schemaexport] Track_id INTEGER NOT NULL IDENTITY,

[schemaexport] title VARCHAR(255) not null,

[schemaexport] filePath VARCHAR(255) not null,

[schemaexport] playTime TIME,

[schemaexport] added DATE,

[schemaexport] volume SMALLINT

[schemaexport] )

...

[schemaexport] alter table TRACK_ARTISTS add constraint FK72EFDAD84C5F92B foreign

key (TRACK_ID) references TRACK

[schemaexport] alter table TRACK_ARTISTS add constraint FK72EFDAD87395D347

foreign key (ARTIST_ID) references ARTIST

[schemaexport] create index ARTIST_NAME on ARTIST (name)

[schemaexport] create index TRACK_TITLE on TRACK (title))




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 set up the uniqueness constraint on artist names, but that seems to be an implementation quirk in HSQLDB itself, and this approach will work just fine.


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




4.2 Persisting Collections
Our first task is to beef up the CreateTest class to take advantage of the new richness in our schema, creating some artists and associating them with tracks.

Example 4-5. Utility methods to help find and create artists, and to link them to tracks

1 package com.oreilly.hh;

2

3 import net.sf.hibernate.*;

4

5 import net.sf.hibernate.cfg.Configuration;

6

7 import java.sql.Time;

8 import java.util.*;

9

10 /**

11 * Create more sample data, letting Hibernate persist it for us.

12 */

13 public class CreateTest {

14

15 /**

16 * Look up an artist record given a name.

17 * @param name the name of the artist desired.

18 * @param create controls whether a new record should be created if

19 * the specified artist is not yet in the database.

20 * @param session the Hibernate session that can retrieve data

21 * @return the artist with the specified name, or <code>null</code> if no

22 * such artist exists and <code>create</code> is <code>false</code>.

23 * @throws HibernateException if there is a problem.

24 */

25 public static Artist getArtist(String name, boolean create,

26 Session session)

27 throws HibernateException

28{

29 Query query = session.getNamedQuery(

30 "com.oreilly.hh.artistByName");

31 query.setString("name", name);

32 Artist found = (Artist)query.uniqueResult();

33 if (found == null && create) {

34 found = new Artist(name, new HashSet());

35 session.save(found);

36 }

37 return found;

38 }

39

40 /**

41 * Utility method to associate an artist with a track

42 */

43 private static void addTrackArtist(Track track, Artist artist) {

44 track.getArtists().add(artist);

45 }




As is so often the case when working with Hibernate, this code is pretty simple and self explanatory. (Do notice that line 8 has changed—we used to import java.util.Date, but we're now importing the whole util package to work with Collections. The '*' is bold to highlight this, but it's easy to miss when scanning the example.)

We'll want to reuse the same artists if we create multiple tracks for them— that's the whole point of using an Artist object rather than just storing strings—so our getArtist() method (starting at line 15) does the work of looking them up by name. The uniqueResult() method it uses on line 32 is a convenience feature of the Query interface, perfect in situations like this, where we know we'll either get one result or none. It saves us the trouble of getting back a list of results, checking the length and extracting the first result if it's there. We'll either get back the single result or null if there were none. (We'd be thrown an exception if there were more than one result, but our unique constraint on the column will prevent that.)

So all we need to do is check for null on line 33, and create a new Artist (lines 34-35) if we didn't find one and we're supposed to.

If we left out the session.save() call, our artists would remain transient. (Itinerant painters? Sorry.) Hibernate is helpful enough to throw an exception if we try to commit our transaction in this state, by detecting references from persistent Track instances to transient Artist instances.






The addTrackArtist() method (lines 40-45) is almost embarrassingly simple. It's just ordinary Java Collections code that grabs the Set of artists belonging to a Track and adds the specified Artist to it. Can that really do everything we need? Where's all the database manipulation code we normally have to write? Welcome to the wonderful world of objectrelational mapping tools!

You might have noticed that getArtist() uses a named query to retrieve the Artist record. In Example 4-6, we will add that at the end of Artist.hbm.xml (actually, we could put it in any mapping file, but this is the most sensible place, since it relates to Artist records).

Example 4-6. Artist lookup query to be added to the artist mapping document

<query name="com.oreilly.hh.artistByName">

<![CDATA[

from com.oreilly.hh.Artist as artist

where upper(artist.name) = upper(:name)

]]>

</query>


We use the upper() function to perform case-insensitive comparison of artists' names, so that we retrieve the artist even if the capitalization is different during lookup than what's stored in the database. This sort of caseinsensitive but preserving architecture, a user-friendly concession to the way humans like to work, is worth implementing whenever possible. Databases other than HSQLDB may have a different name for the function that converts strings to uppercase, but there should be one available.

Now we can use this infrastructure to actually create some tracks with linked artists. Example 4-7 shows the remainder of the CreateTest class with the additions marked in bold. Edit your copy to match (or download it to save the typing).

Example 4-7. Revisions to main method of CreateTest.java in order to add artist associations

1 public static void main(String args[]) throws Exception {

2 // Create a configuration based on the properties file we've put

3 // in the standard place.

4 Configuration config = new Configuration();

5

6 // Tell it about the classes we want mapped, taking advantage of

7 // the way we've named their mapping documents.

8 config.addClass(Track.class).addClass(Artist.class);

9

10 // Get the session factory we can use for persistence

11 SessionFactory sessionFactory = config.buildSessionFactory();

12

13 // Ask for a session using the JDBC information we've configured

14 Session session = sessionFactory.openSession();

15Transaction tx = null;

16 try {

17 // Create some data and persist it

18 tx = session.beginTransaction();

19

20 Track track = new Track("Russian Trance",

21 "vol2/album610/track02.mp3",

22 Time.valueOf("00:03:30"), new Date(),

23 (short)0, new HashSet());

24 addTrackArtist(track, getArtist("PPK", true, session));

25 session.save(track);

26

27 track = new Track("Video Killed the Radio Star",

28 "vol2/album611/track12.mp3",

29 Time.valueOf("00:03:49"), new Date(),

30 (short)0, new HashSet());

31 addTrackArtist(track, getArtist("The Buggles", true, session));

32 session.save(track);

33

34

35 track = new Track("Gravity's Angel",

36 "vol2/album175/track03.mp3",

37 Time.valueOf("00:06:06"), new Date(),

38 (short)0, new HashSet());

39 addTrackArtist(track, getArtist("Laurie Anderson", true, session));

40 session.save(track);

41

42 track = new Track("Adagio for Strings (Ferry Corsten Remix)",

43 "vol2/album972/track01.mp3",

44 Time.valueOf("00:06:35"), new Date(),

45 (short)0, new HashSet());

46 addTrackArtist(track, getArtist("William Orbit", true, session));

47 addTrackArtist(track, getArtist("Ferry Corsten", true, session));

48 addTrackArtist(track, getArtist("Samuel Barber", true, session));

49 session.save(track);

50

51 track = new Track("Adagio for Strings (ATB Remix)",

52 "vol2/album972/track02.mp3",

53 Time.valueOf("00:07:39"), new Date(),

54 (short)0, new HashSet());

55 addTrackArtist(track, getArtist("William Orbit", true, session));

56 addTrackArtist(track, getArtist("ATB", true, session));

57 addTrackArtist(track, getArtist("Samuel Barber", true, session));

58 session.save(track);

59

60 track = new Track("The World '99",

61 "vol2/singles/pvw99.mp3",

62 Time.valueOf("00:07:05"), new Date(),

63 (short)0, new HashSet());

64addTrackArtist(track, getArtist("Pulp Victim", true, session));

65addTrackArtist(track, getArtist("Ferry Corsten", true, session));

66 session.save(track);

67

68 track = new Track("Test Tone 1",

69 "vol2/singles/test01.mp3",

70 Time.valueOf("00:00:10"), new Date(),

71 (short)0, new HashSet());

72 session.save(track);

73

74 // We're done; make our changes permanent

75 tx.commit();

76

77} catch (Exception e) {

78 if (tx != null) {

79 // Something went wrong; discard all partial changes

80 tx.rollback();

81 }

82 throw e;

83} finally {

84 // No matter what, close the session

85 session.close();

86}

87

88// Clean up after ourselves

89sessionFactory.close();

90 }

91 }




The changes to the existing code are pretty minimal. First we need to map our new Artist class, which takes just one method call on line 8 (again, thanks to the naming convention we've been following to link our mapping documents to their classes). The lines that created the three tracks from Chapter 3 need only a single new parameter each, to supply an initially empty set of Artist associations (lines 23, 30, and 38). Each also gets a new follow-up line establishing an association to the artist for that track. We could have structured this code differently, by writing a helper method to create the initial HashSet containing the artist, so we could do this all in one line. The approach we actually used scales better to multi-artist tracks, as the next section illustrates.

The largest chunk of new code, lines 42-66, simply adds three new tracks to show how multiple artists per track are handled. If you like electronica and dance remixes (or classical for that matter), you know how important an issue that can be. Because we set the links up as collections, it's simply a matter of adding each artist link to the tracks. Finally, lines 68-72 add a track with no artist associations to see how that behaves, too. Now you can run ant ctest to create the new sample data containing tracks, artists, and associations between them.

A useful trick if you're making changes to your test data creation program and you want to try it again starting from an empty database is to issue the command ant schema ctest. This tells Ant to run the schema and ctest targets one after the other. Running schema blows away any existing data; then ctest gets to create it anew.






NOTE


Of course, in real life you'd be getting this data into the database in some other way—through a user interface, or as part of the process of importing the actual


4.3 Retrieving Collections
Example 4-8. QueryTest.java enhanced in order to display artists associated with tracks

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 * Retrieve data as objects

11 */

12 public class QueryTest {

13

14 /**

15 * Retrieve any tracks that fit in the specified amount of time.

16 *

17 * @param length the maximum playing time for tracks to be returned.

18 * @param session the Hibernate session that can retrieve data.

19 * @return a list of {@link Track}s meeting the length restriction.

20 * @throws HibernateException if there is a problem.

21 */

22 public static List tracksNoLongerThan(Time length, Session session)

23 throws HibernateException

24 {

25 Query query = session.getNamedQuery(

26 "com.oreilly.hh.tracksNoLongerThan");

27 query.setTime("length", length);

28 return query.list();

29 }

30

31 /**

32 * Build a parenthetical, comma-separated list of artist names.

33 * @param artists the artists whose names are to be displayed.

34 * @return formatted list, or an empty string if the set was empty.

35 */

36 public static String listArtistNames(Set artists) {

37 StringBuffer result = new StringBuffer();

38 for (Iterator iter = artists.iterator(); iter.hasNext(); ) {

39 Artist artist = (Artist)iter.next();

40 result.append((result.length() == 0) ? "(" : ", ");

41 result.append(artist.getName());

42 }

43 if (result.length() > 0) {

44 result.append(") ");

45 }

46 return result.toString();

47 }

48

49 /**

50 * Look up and print some tracks when invoked from the command line.

51 */

52 public static void main(String args[]) throws Exception {

53 // Create a configuration based on the properties file we've put

54 // in the standard place.

55 Configuration config = new Configuration();

56

57 // Tell it about the classes we want mapped, taking advantage of

58 // the way we've named their mapping documents.

59 config.addClass(Track.class).addClass(Artist.class);

60

61 // Get the session factory we can use for persistence

62 SessionFactory sessionFactory = config.buildSessionFactory();

63

64 // Ask for a session using the JDBC information we've configured

65 Session session = sessionFactory.openSession();

66 try {

67 // Print the tracks that will fit in seven minutes

68 List tracks = tracksNoLongerThan(Time.valueOf("00:07:00"),

69 session);

70 for (ListIterator iter = tracks.listIterator() ;

71 iter.hasNext() ; ) {

72 Track aTrack = (Track)iter.next();

73 System.out.println("Track: \"" + aTrack.getTitle() + "\" " +

74 listArtistNames(aTrack.getArtists()) +

75 aTrack.getPlayTime());

76 }

77 } finally {

78 // No matter what, close the session

79 session.close();

80 }

81

82 // Clean up after ourselves

83 sessionFactory.close();

84 }

85 }




The first thing we add is a little utility method (lines 31-47) to format the set of artist names nicely, as a comma-delimited list inside parentheses, with proper spacing, or as nothing at all if the set of artists is empty.

NOTE


How easy was that?


Next, as with CreateTest, we need to tell Hibernate to map our new Artist class on line 59. Since all the interesting new multi-artist tracks are longer than five minutes, we increase the cutoff in our query to seven minutes so we can see some (line 68). Finally we call listArtistNames() at the proper position in the println() statement describing the tracks found (line 74).

Example 4-9 shows the new output from ant qtest .

Example 4-9. QueryTest output with artist information

% ant qtest

Buildfile: build.xml



prepare:



compile:

[javac] Compiling 1 source file to /Users/jim/Documents/Work/OReilly/

Hibernate/Examples/ch04/classes



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



BUILD SUCCESSFUL

Total time: 17 seconds

11.940u 1.510s 0:18.06 74.4% 0+0k 0+7io 0pf+0w




You'll notice two things. First, that this is much easier to interpret than the columns of numbers in Figure 4-2. And second, it worked! Even in the 'tricky' case of the test tone track without any artist mappings, Hibernate takes the friendly approach of creating an empty artists Set, sparing us from peppering our code with the null checks we'd otherwise need to avoid crashing with NullPointerExceptions.

NOTE


But wait, there's more! No additional code needed...

4.4 Using Bidirectional Associations
In our creation code, we established links from tracks to artists, simply by adding Java objects to appropriate collections. Hibernate did the work of translating these associations and groupings into the necessary cryptic entries in a join table it created for that purpose. It allowed us with easy, readable code to establish and probe these relationships. But remember that we made this association bidirectional—the Artist class has a collection of Track associations too. We didn't bother to store anything in there.

The great news is that we don't have to. Because of the fact that we marked this as an inverse mapping in the Artist mapping document, Hibernate understands that when we add an Artist association to a Track, we're implicitly adding that Track as an association to the Artist at the same time.

This convenience works only when you make changes to the 'primary' mapping, in which case they propagate to the inverse mapping. If you make changes only to the inverse mapping, in our case the Set of tracks in the Artist object, they will not be persisted. This unfortunately means your code must be sensitive to which mapping is the inverse.






Let's build a simple interactive graphical application that can help us check whether the artist to track links really show up. It will let you type in an artist's name, and show you all the tracks associated with that artist. A lot of the code is very similar to our first query test. Create the file QueryTest2.java and enter the code shown in Example 4-10.

Example 4-10. Source for QueryTest2.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 import java.awt.*;

9 import java.awt.event.*;

10 import javax.swing.*;

11

12 /**

13 * Provide a user interface to enter artist names and see their tracks.

14 */

15 public class QueryTest2 extends JPanel {

16

17 JList list; // Will contain tracks associated with current artist

18 DefaultListModel model; // Lets us manipulate the list contents

19

20 /**

21 * Build the panel containing UI elements

22 */

23 public QueryTest2() {

24 setLayout(new BorderLayout());

25 model = new DefaultListModel();

26 list = new JList(model);

27 add(new JScrollPane(list), BorderLayout.SOUTH);

28

29 final JTextField artistField = new JTextField(30);

30 artistField.addKeyListener(new KeyAdapter() {

31 public void keyTyped(KeyEvent e) {

32 SwingUtilities.invokeLater(new Runnable() {

33 public void run() {

34 updateTracks(artistField.getText());

35 }

36 });

37 }

38 });

39 add(artistField, BorderLayout.EAST);

40 add(new JLabel("Artist: "), BorderLayout.WEST);

41 }

42

43 /**

44 * Update the list to contain the tracks associated with an artist

45 */

46 private void updateTracks(String name) {

47 model.removeAllElements(); // Clear out previous tracks

48 if (name.length() < 1) return; // Nothing to do

49 try {

50 // Ask for a session using the JDBC information we've configured

51 Session session = sessionFactory.openSession();

52 try {

53 Artist artist = CreateTest.getArtist(name, false, session);

54 if (artist == null) { // Unknown artist

55 model.addElement("Artist not found");

56 return;

57 }

58 // List the tracks associated with the artist

59 for (Iterator iter = artist.getTracks().iterator() ;

60 iter.hasNext() ; ) {

61 Track aTrack = (Track)iter.next();

62 model.addElement("Track: \"" + aTrack.getTitle() +

63 "\", " + aTrack.getPlayTime());

64 }

65 } finally {

66 // No matter what, close the session

67 session.close();

68 }

69 } catch (Exception e) {

70 System.err.println("Problem updating tracks:" + e);

71 e.printStackTrace();

72 }

73 }

74

75 private static SessionFactory sessionFactory; // Used to talk to Hibernate

76

77 /**

78 * Set up Hibernate, then build and display the user interface.

79 */

80 public static void main(String args[]) throws Exception {

81 // Load configuration properties, read mappings for persistent classes.

82 Configuration config = new Configuration();

83 config.addClass(Track.class).addClass(Artist.class);

84

85 // Get the session factory we can use for persistence

86 sessionFactory = config.buildSessionFactory();

87

88 // Set up the UI

89 JFrame frame = new JFrame("Artist Track Lookup");

90 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

91 frame.setContentPane(new QueryTest2());

92 frame.setSize(400, 180);

93 frame.setVisible(true);

94 }

95 }




The bulk of the novel code in this example deals with setting up a Swing user interface. It's actually a rather primitive interface, and won't resize nicely, but dealing with such details would make the code larger, and really falls outside the scope of this book. If you want examples of how to build rich, quality Swing interfaces, check out our Java Swing, Second Edition (O'Reilly). It's much thicker so it has room for all that good stuff.

NOTE


Yes, this is a shameless plug.


The only item I want to highlight in the constructor is the KeyListener that gets added to artistField (lines 30-38). This rather tricky bit of code creates an anonymous class whose keyTyped() method is invoked whenever the user types in the artist text field. That method, lines 31- 37, tries to update the track display by checking whether the field now contains a recognized artist name. Unfortunately, at the time the method gets invoked, the text field has not yet been updated to reflect the latest keystroke, so we're forced to defer the actual display update to a second anonymous class (the Runnable instance created on lines 32-36) via the invokeLater() method of SwingUtilities. This technique causes the update to happen when Swing 'gets around to it,' which in our case means the text field will have finished updating itself.

The updateTracks() method that gets called at that point is where the interesting Hibernate stuff happens. It starts by clearing the list on line 47, discarding any tracks it might have previously been displaying. If the artist name is empty, that's all it does. Otherwise, it opens a Hibernate session on line 51 and tries to look up the artist using the getArtist() method we wrote in CreateTest. This time we tell it not to create an artist if it can't find the one we asked for, so we'll get back a null if the user hasn't typed the name of a known artist. If that's the case, we just display a message to that effect (line 55).

If we do find an Artist record, on the other hand, line 59 iterates over any Track records found in the artist's set of associated tracks, and lines 61-63 display information about each one. All this will test whether the inverse association has worked the way we'd like it to. Finally (no pun intended), lines 65-68 make sure to close the session when we're leaving the method, even through an exception. You don't want to leak sessions—that's a good way to bog down and crash your whole database environment.

The main() method starts out with the same Hibernate configuration steps we've seen before in lines 81-86, then creates and displays the user interface frame in lines 89-93. Line 90 sets the interface up to end the program when it's closed. After displaying the frame, main() returns. From that point on, the Swing event loop is in control.

Once you've created (or downloaded) this source file, you also need to add a new target, shown in Example 4-11, to the end of build.xml (the Ant build file) to invoke this new class.

Example 4-11. Ant target for running the new query test

<target name="qtest2" description="Run a simple Artist exploration GUI"

depends="compile">

<java classname="com.oreilly.hh.QueryTest2" fork="true">

<classpath refid="project.class.path"/>

</java>

</target>




NOTE


This is very similar to the existing 'qtest' target; copy and tweak that.


Now you can fire it up by typing ant qtest2 and play with it for yourself. Figure 4-3 shows the program in action, displaying tracks for one of the artists in our sample data.


Figure 4-3. A simple artist tracks browser

4.5 Working with Simple Collections
The collections we've been looking at so far have all contained associations to other objects, which is appropriate for a chapter titled 'Collections and Associations,' but isn't the only kind you can use with Hibernate. You can also define mappings for collections of simple values, like strings, numbers, and nonpersistent value classes.

4.5.1 How do I do that?
Suppose we want to be able to record some number of comments about each track in the database. We want a new property called comments to contain the String values of each associated comment. The new mapping in Tracks.hbm.xml looks a lot like what we did for artists, only a bit simpler:


<set name="comments" table="TRACK_COMMENTS">

<key column="TRACK_ID"/>

<element column="COMMENT" type="string"/>

</set>




Since we're able to store an arbitrary number of comments for each Track, we're going to need a new table to put them in. Each comment will be linked to the proper Track through the track's id property.

Rebuilding the databases with ant schema shows how this gets built in the database:


[schemaexport] create table TRACK_COMMENTS (

[schemaexport] TRACK_ID INTEGER not null,

[schemaexport] COMMENT VARCHAR(255)

[schemaexport] )

[schemaexport] alter table TRACK_COMMENTS add constraint FK105B26884C5F92B

foreign key (TRACK_ID) references TRACK




NOTE


Data modeling junkies will recognize this as a 'one-to-many' relationship.


After updating the Track class via ant codegen , we need to add another Set at the end of each constructor invocation in CreateTest.java, for the comments. For example:


track = new Track("Test Tone 1",

"vol2/singles/test01.mp3",

Time.valueOf("00:00:10"), new Date(),

(short)0, new HashSet(), new HashSet());




Then we can assign a comment on the following line:


track.getComments().add("Pink noise to test equalization");




A quick ant ctest will compile and run this (making sure you've not forgotten to add the second HashSet to any tracks), and you can check data/music.script to see how it's stored in the database. Or add another loop after the track println() in QueryTest.java to print the comments for the track that was just displayed:


for (Iterator comIter = aTrack.getComments().iterator() ;

comIter.hasNext() ; ) {

System.out.println(" Comment: " + comIter.next());

}




Then ant qtest will give you output like this:


...

[java] Track: "Test Tone 1" 00:00:10

[java] Comment: Pink noise to test equalization

No comments:

Post a Comment