Thursday, February 4, 2010

Section 6.4. Working with Persistent Enumerations








6.4. Working with Persistent Enumerations


You may have noticed that we never defined a persistence mapping
for the SourceMedia
class in the first part of this chapter. That's because our enumerated
type is a value that gets persisted as
part of one or more entities, rather than being an entity unto
itself.


In that light, it's not surprising that we've not yet done any
mapping. That happens when it's time to actually use the persistent
enumeration—which is to say, now.



6.4.1. How do I do that?


Recall that we wanted to keep track of the source media for the
music tracks in our jukebox system. That means we want to use the
SourceMedia enumeration in our
Track mapping. We can simply add a new <property> tag to the <class> definition in Track.hbm.xml, as shown in Example 6-3.


Example 6-3. Adding the sourceMedia property to the track mapping
document



...
<property name="volume" type="short">
<meta attribute="field-description">How loud to play the track</meta>
</property>

<property name="sourceMedia" type="com.oreilly.hh.SourceMediaType">
<meta attribute="field-description">Media on which track was obtained</meta>
<meta attribute="use-in-tostring">true</meta>
</property>


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





Notice that we've told Hibernate that the type of this property is
our UserType implementation, not the raw
enumeration type that it is responsible for helping persist. Because the
<type> of our sourceMedia property names
a class that implements the UserType interface, Hibernate knows to
delegate to that class to perform persistence, as well as for
discovering the Java and SQL types associated with the mapping.


Now, running ant codegen
updates our Track class to include the new
property….




6.4.2. Not so fast!


During development of this chapter, I ran into a strange problem
where suddenly my code wouldn't compile anymore, due to complaints about
constructors not being found. At first it seemed to be somehow related
to adopting the Maven Ant Tasks for dependency management, because it
first happened when I was testing that. Even looking closely at the
code, it took me a while to see what was wrong, because it was subtle.
The sourceMedia property in
Track was being assigned the type SourceMediaType (the mapping manager),
rather than SourceMedia like it should be.


After we all flailed around for a while and I posted a confused
bug report to the Hibernate Tools team, which they quite rightly
reported being unable to reproduce, I figured out what was happening.
The build was broken: the Hibernate Tools need to be able to find the
compiled SourceMediaType class in order to make
sense of the mapping document and realize that it is a user type. As I
was writing the text, I had written and compiled
SourceMediaType first, so it was there when I
updated the mapping to look like Example 6-3
and invoked the <codegen>
target. But when I came back and was testing with the Maven ant tasks, I
was starting with no compiled classes, just like you would after
downloading the code examples archive, and the creation and query tests
had already been updated as described in the next few sections. However,
in that context, running <codegen>
before <compile> leaves you in a
situation where the classes are inconsistent and can't compile. And you
can't run <compile> before <codegen> because those test classes are
dependent on the existence of the generated data classes.


NOTE


Sure sounds like a classic catch-22.



This kind of head-spinning circular dependency problem is, sadly,
not uncommon when you've not been paying attention to maintaining your
build instructions. I'd introduced a new dependency for the <codegen> target without encoding it in the
build.xml. We wasted a fair amount
of time barking up the wrong trees, but it did give me a chance to
describe the problem and the solution, so hopefully you will be smarter
if you find yourself in a similar situation.


Once the problem was clearly understood, it wasn't difficult to
solve. Example 6-4 shows the changes needed in build.xml.


Example 6-4. Expressing the UserType dependencies in the build
process



  <!-- Compile the UserType definitions so they can be used in the code
generation phase. -->
<target name="usertypes" depends="prepare"

description="Compile custom type definitions needed in by codegen">
<javac srcdir="${source.root}"
includes="com/oreilly/hh/*Type.java"
destdir="${class.root}"
debug="on"
optimize="off"
deprecation="on">
<classpath refid="project.class.path"/>
</javac>
</target>


<!-- Generate the java code for all mapping files in our source tree -->
<target name="codegen" depends="usertypes"
description="Generate Java source from the O/R mapping files">






NOTE


Phew!



With these additions in place, running ant codegen now correctly updates our
Track class to include the new property. The
signature of the full-blown Track constructor now
looks like this:


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



We need to make corresponding changes in CreateTest.java:

      Track track = new Track("Russian Trance",                                 
"vol2/album610/track02.mp3",
Time.valueOf("00:03:30"),
new HashSet<Artist>(),
new Date(), (short)0, SourceMedia.CD,
new HashSet<String>());
...
track = new Track("Video Killed the Radio Star",
"vol2/album611/track12.mp3",
Time.valueOf("00:03:49"), new HashSet<Artist>(),
new Date(), (short)0, SourceMedia.VHS,
new HashSet<String>());




And so on. To get the results shown in Figure 6-1, we mark
the rest as coming from CDs, except for "The World
'99," which comes from a stream, and give "Test Tone 1" a null sourceMedia value. At this point, run
ant schema to rebuild the database
schema with support for the new property, and run ant ctest to create the sample data.




6.4.3. What just happened?


Our TRACK table now contains a
column to store the sourceMedia
property. We can see its values by looking at the contents of the table
after creating the sample data (the easiest way is to run a query within
ant db, as shown in Figure 6-1).


We can verify that the values persisted to the database are
correct by cross-checking the codes assigned to our persistent
enumeration. Leveraging Java 5's enum
features allows even this raw query to be pretty meaningful.



Figure 6-1. Source media information in the TRACK table










6.4.4. Why didn't it work?


By introducing these custom types to our mapping documents, we've
introduced another new dependency that we have not yet reflected in
build.xml. So, if you weren't
following along carefully, and failed to run ant compile before ant schema, you will have received some
complaints like this from Hibernate:


[hibernatetool] INFO: Using dialect: org.hibernate.dialect.HSQLDialect
[hibernatetool] An exception occurred while running exporter #2:hbm2ddl (Generat
es database schema)
[hibernatetool] To get the full stack trace run ant with -verbose
[hibernatetool] org.hibernate.MappingException: Could not determine type for: co
m.oreilly.hh.StereoVolumeType, for columns: [org.hibernate.mapping.Column(VOL_LE
FT), org.hibernate.mapping.Column(VOL_RIGHT)]

BUILD FAILED
/Users/jim/Documents/Work/OReilly/svn_hibernate/current/examples/ch07/build.xml:
81: org.hibernate.MappingException: Could not determine type for: com.oreilly.hh
.StereoVolumeType, for columns: [org.hibernate.mapping.Column(VOL_LEFT), org.hib
ernate.mapping.Column(VOL_RIGHT)]

Total time: 3 seconds


This is because, without compiling our new custom types,
Hibernate can't find or use them, so the mappings don't make sense.
As a quick fix, just run ant compile
and then try ant schema again. We
should also fix this in build.xml
so that it can't bite anyone else in the future:


  <!-- Generate the schemas for all mapping files in our class tree -->
<target name="schema" depends="compile"
description="Generate DB schema from the O/R mapping files">
...



It doesn't matter that the <compile> target comes later in the file than
<schema>; Ant will sort this out just
fine. If it bothers you, feel free to swap them. To be completely
thorough about this we can also make <compile> depend on <codegen>, to ensure that the data classes are
generated before we try to compile everything:


  <!-- Compile the java source of the project -->
<target name="compile" depends="codegen"
description="Compiles all Java classes">
...



With that set of chained dependencies, you can start with a bare
source directory, and generate and compile everything in one fell
swoop:


% ant compile
Buildfile: build.xml

prepare:
[copy] Copying 3 files to /Users/jim/svn/oreilly/hib_dev_2e/current/example
s/ch07/classes

usertypes:
[javac] Compiling 2 source files to /Users/jim/svn/oreilly/hib_dev_2e/curren
t/examples/ch07/classes

codegen:
[hibernatetool] Executing Hibernate Tool with a Standard Configuration
[hibernatetool] 1. task: hbm2java (Generates a set of .java files)

compile:
[javac] Compiling 8 source files to /Users/jim/svn/oreilly/hib_dev_2e/curren
t/examples/ch07/classes

BUILD SUCCESSFUL
Total time: 3 seconds


OK, let's get back to learning about custom types….


We can see an even more friendly version of the information (and
incidentally test the retrieval half of our custom persistence helper)
by slightly enhancing the query test to print the descriptions
associated with this property for the tracks it retrieves. The necessary
changes are shown in bold in Example 6-5.


Example 6-5. Displaying source media in QueryTest.java



...
// Print the tracks that will fit in seven minutes
List tracks = tracksNoLongerThan(Time.valueOf("00:07:00"),
session);
for (ListIterator iter = tracks.listIterator() ;
iter.hasNext() ; ) {
Track aTrack = (Track)iter.next();
String mediaInfo = "";
if (aTrack.getSourceMedia() != null) {
mediaInfo = ", from " +
aTrack.getSourceMedia().getDescription();
}

System.out.println("Track: \"" + aTrack.getTitle() + "\" " +
listArtistNames(aTrack.getArtists()) +
aTrack.getPlayTime() + mediaInfo);
...




With these enhancements, running ant
qtest
yields the output shown in Example 6-6. Tracks
with non-null source media values now
have "from" and the appropriate media description displayed at the
end.


Example 6-6. Human-oriented display of source media information



...
qtest:
[java] Track: "Russian Trance" (PPK) 00:03:30, from Compact Disc
[java] Track: "Video Killed the Radio Star" (The Buggles) 00:03:49, from VH
S Videocassette tape
[java] Track: "Gravity's Angel" (Laurie Anderson) 00:06:06, from Compact Di
sc
[java] Track: "Adagio for Strings (Ferry Corsten Remix)" (William Orbit, Fe
rry Corsten, Samuel Barber) 00:06:35, from Compact Disc
[java] Track: "Test Tone 1" 00:00:10
[java] Comment: Pink noise to test equalization



Note that if we hadn't decided to do our own fancy formatting of a
subset of the tracks' properties in QueryTest and
instead relied on the toString⁠⁠(⁠ ⁠) method in
Track, we would not have needed to make any
changes to QueryTest to see this new information,
although we'd have seen the same minimalist version of the enumeration
names as in the database query. Our mapping document specified that the
sourceMedia property should be
included in the toString⁠⁠(⁠ ⁠) result, which
would have taken care of it. You can inspect the generated
toString⁠⁠(⁠ ⁠) source to check this, or write
a simple test program to see what the
toString⁠⁠(⁠ ⁠) output looks like. An excellent
strategy would be to fix AlbumTest.java so it will compile and run
after our changes to Track. The easiest fix is to
simply hard-code the addAlbumTrack⁠⁠(⁠ ⁠)
method to assume everything comes from CDs, as in
Example 6-7 (the JavaDoc already excuses such shameful
rigidity).


Example 6-7. Fixing AlbumTest.java to support source media



    /**
* 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, SourceMedia.CD,
new HashSet<String>());
track.getArtists().add(artist);
// session.save(track);
album.getTracks().add(new AlbumTrack(track, disc, positionOnDisc));
}





With this fix in place, running ant
atest
shows that the source media information propagates all
the way up to Album's own
toString⁠⁠(⁠ ⁠) method:


     [java] com.oreilly.hh.data.Album@ccad9c [title='Counterfeit e.p.' tracks='[
com.oreilly.hh.data.AlbumTrack@9c0287 [track='com.oreilly.hh.data.Track@6a21b2 [
title='Compulsion' sourceMedia='CD' ]' ], com.oreilly.hh.data.AlbumTrack@aa8eb7
[track='com.oreilly.hh.data.Track@7fc8a0 [title='In a Manner of Speaking' source
Media='CD'
]' ], com.oreilly.hh.data.AlbumTrack@4cadc4 [track='com.oreilly.hh.da
ta.Track@243618 [title='Smile in the Crowd' sourceMedia='CD' ]' ], com.oreilly.h
h.data.AlbumTrack@5b644b [track='com.oreilly.hh.data.Track@157e43 [title='Gone'
sourceMedia='CD'
]' ], com.oreilly.hh.data.AlbumTrack@1483a0 [track='com.oreilly
.hh.data.Track@cdae24 [title='Never Turn Your Back on Mother Earth' sourceMedia=
'CD'
]' ], com.oreilly.hh.data.AlbumTrack@63dc28 [track='com.oreilly.hh.data.Tra
ck@ae511 [title='Motherless Child' sourceMedia='CD' ]' ]]' ]


With a little work, Hibernate lets you extend your typesafe
enumerations to support persistence. And once you've invested that
effort, you can persist them as easily as any other value type for which
native support exists.


It would be nice if the native type support in Hibernate evolved
to take advantage of the robust enum
keyword support in Java 5 out of the box, though I don't hold out much
hope since Java 5 has been out for a while now. But, as far as gripes
go, this is a mild one, and you can take your pick of enum-supporting user type implementations on
the Hibernate wiki.


Now let's move into mappings that are complex and idiosyncratic
enough that nobody would expect Hibernate to build in support for
them.










No comments: