Hibernate supports a wealth of Java types—both simple values and objects—as you can see by skimming Appendix A. By setting up mapping specifications, you can persist even highly complex, nested object structures to arbitrary database tables and columns. With all this power and flexibility, you might wonder why you’d ever need to go beyond the built-in type support.
One situation that might motivate you to customize Hibernate’s
type support is if you want to use a different
SQL column type to store a particular Java type than
Hibernate normally chooses. The reference documentation cites the example
of persisting Java BigInteger
values into VARCHAR
columns, which might be necessary
to accommodate a legacy database schema.
Another scenario that is very common involves persisting
enumerated type values. Prior to Java 5, there was no built-in
language support for enumerations, so although Joshua Bloch’s excellent
pattern presented in Effective Java Programming Language
Guide (Addison-Wesley) was a de facto standard, Hibernate had
to be agnostic about how to support the concept. The
PersistentEnum
interface that it provided prior to
Hibernate 3 wasn’t suited to the native enum
support introduced in Java 5, so it has
gone away. Unfortunately, nothing has arisen to replace it within
Hibernate itself, so we’ll show you how to leverage the user type support
for this purpose.
Another scenario that requires the ability to tweak the type system
is when you have a single property value that needs to be split into more
than one database column—maybe the Address
object
in your company’s mandated reuse library stores ZIP+4
codes as a single string, but the database to which you’re integrating
contains a required five digit column and a separate nullable four digit
column for the two components. Or, maybe it’s the other way around, and
you need to separate a single database column into more than one
property.
Luckily, in situations like this, Hibernate lets you take over the details of the persistence mapping so you can fit square pegs into round holes when you really need to.
Continuing in the spirit of making simple things easy and complex things possible....
You might also want to build a custom value type even in some cases where it’s not strictly necessary. If you’ve got a composite type that is used in many places throughout your application (a vector, complex number, address, or the like), you can certainly map each of these occurrences as components, but it might be worth encapsulating the details of the mapping in a shared, reusable Java class rather than propagating the details throughout each of the mapping documents. That way, if the details of the mapping ever need to change for any reason, you’ve only got one class to fix rather than many individual component mappings to hunt down and adjust.
In all of these scenarios, the task is to teach Hibernate a new way to translate between a particular kind of in-memory value and its persistent database representation.
Hibernate lets you provide your own logic for mapping values in
situations that need it, by implementing one of two interfaces:
org.hibernate.usertype.UserType
or
org.hibernate.usertype.CompositeUserType
.
It’s important to realize that what is being created is a
translator for a particular kind of
value, not a new kind of value that knows how to persist itself. In
other words, in our ZIP code example, it’s not the
ZIP code property that would implement
UserType
. Instead, we’d create a new class
implementing UserType
, and in our mapping
document, specify this class as the Java type used to map the
ZIP code property. Because of this, I think the
terminology of “user types” is a little confusing.
Let’s look at a concrete example. As noted above, a very common
goal is to persist an enumerated type, and though Hibernate doesn’t
natively support this, we can leverage the UserType
mechanism to achieve it fairly easily. Later in this chapter we’ll look
at a more complex mapping example involving multiple properties and
columns.
An enumerated type is a common and
useful programming abstraction allowing a value to be selected
from a fixed set of named choices. These were originally well represented
in Pascal, but C took such a minimal approach (essentially just letting
you assign symbolic names to interchangeable integer values) that early
Java releases reserved C’s enum
keyword but
declined to implement it. A better, object-oriented approach known as the
“typesafe enum pattern” evolved and was popularized in Joshua Bloch’s
Effective Java. This approach required a fair
amount of boilerplate coding, but it lets you do all kinds of interesting
and powerful things. One of the many delightful innovations in the Java 5
specification was the ability to resuscitate the enum
keyword as an easy way to get the power of
typesafe enumerations without all the tedious boilerplate coding, and it
provides other nifty benefits.
C-style numeric “enumerations” still appear too often in Java. Older parts of the Sun API contain many of them.
Regardless of how you implement an enumerated type, you’re sometimes
going to want to be able to persist such values to a database. And even
though there is now a standard way to do it in Java, Hibernate doesn’t
offer any built-in support. So let’s see how to implement a
UserType
that can persist an enum
for us.
Let’s suppose we want to be able to specify whether our tracks came from cassette tapes, vinyl, VHS tapes, CDs, a broadcast, an Internet download site, or a digital audio stream. (We could go really nuts and distinguish between Internet streams and satellite radio services like Sirius or XM, or radio versus television broadcast, but this is plenty to demonstrate the important ideas.)
Without any consideration of persistence, our typesafe enumeration class might look something like Example 6-1. (The JavaDoc has been compressed to take less printed space, but the downloadable version is formatted normally.)
package com.oreilly.hh; /** * This is a typesafe enumeration that identifies the media on which an * item in our music database was obtained. */ public enum SourceMedia { /** Music obtained from magnetic cassette tape. */ CASSETTE("Audio Cassette Tape"), /** Music obtained from a vinyl record. */ VINYL("Vinyl Record"), /** Music obtained from VHS tapes. */ VHS("VHS Videocassette tape"), /** Music obtained from a broadcast. */ BROADCAST("Analog Broadcast"), /** Music obtained from a digital compact disc. */ CD("Compact Disc"), /** Music obtained as an Internet download. */ DOWNLOAD("Internet Download"), /** Music obtained from a digital audio stream. */ STREAM("Digital Audio Stream"); /** * Stores the human-readable description of this instance, by which it is * identified in the user interface. */ private final String description; /** * Enum constructors are always private since they can only be accessed * through the enumeration mechanism. * * @param description human readable description of the source for the * audio, by which it is presented in the user interface. */ private SourceMedia(String description) { this.description = description; } /** * Return the description associated with this enumeration instance. * * @return the human-readable description by which this value is * identified in the user interface. **/ public String getDescription() { return description; } }
Of course, the beauty of working with Hibernate is that we don’t
need to change our normal Java classes to add support for persistence.
Even though we’ve not thought about persistence in designing this enum
, we will be able to use it as-is. Now that
we no longer have to consider using the deprecated PersistentEnum
interface, defining a
persistent enumerated type is no different than defining any other
enumerated type.
The PersistentEnum
interface did require
you to change your enumerations for persistence. That was probably an
even bigger reason why it was deprecated than the fact that it did not
mesh well with the Effective Java persistent
enumeration pattern or the Java 5 enum
keyword.
So how do we teach Hibernate to persist values of our enumeration?
As noted in the introduction to this chapter, we are going to
create a class that Hibernate can use when it needs to persist our
enumeration. We’ll call our new class
SourceMediaType
. Our next decision is whether it
needs to implement UserType
or
CompositeUserType
. The reference documentation
doesn’t provide much guidance on this question, but the
API documentation confirms the hint contained in
the interface names: the
CompositeUserType
interface is only needed if your
custom type implementation wants to expose internal structure in the form
of named properties that can be accessed individually in queries (as in
our ZIP code example). For
SourceMedia
, a simple
UserType
implementation is sufficient. The source
for a mapping manager meeting our needs is shown in Example 6-2.
package com.oreilly.hh; import java.io.Serializable; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; import org.hibernate.Hibernate; import org.hibernate.HibernateException; import org.hibernate.usertype.UserType; /** * Manages persistence for the {@link SourceMedia} typesafe enumeration. */ public class SourceMediaType implements UserType { /** * Indicates whether objects managed by this type are mutable. * * @return <code>false</code>, since enumeration instances are immutable * singletons. */ public boolean isMutable() { return false; } /** * Return a deep copy of the persistent state, stopping at * entities and collections. * * @param value the object whose state is to be copied. * @return the same object, since enumeration instances are singletons. */ public Object deepCopy(Object value) { return value; } /** * Compare two instances of the class mapped by this type for persistence * "equality". * * @param x first object to be compared. * @param y second object to be compared. * @return <code>true</code> iff both represent the same SourceMedia type. * @throws ClassCastException if x or y isn't a {@link SourceMedia}. */ public boolean equals(Object x, Object y) { // We can compare instances, since SourceMedia are immutable singletons return (x == y); } /** * Determine the class that is returned by {@link #nullSafeGet}. * * @return {@link SourceMedia}, the actual type returned * by {@link #nullSafeGet}. */ public Class returnedClass() { return SourceMedia.class; } /** * Determine the SQL type(s) of the column(s) used by this type mapping. * * @return a single VARCHAR column. */ public int[] sqlTypes() { // Allocate a new array each time to protect against callers changing // its contents. int[] typeList = new int[1]; typeList[0] = Types.VARCHAR; return typeList; } /** * Retrieve an instance of the mapped class from a JDBC {@link ResultSet}. * * @param rs the results from which the instance should be retrieved. * @param names the columns from which the instance should be retrieved. * @param owner the entity containing the value being retrieved. * @return the retrieved {@link SourceMedia} value, or <code>null</code>. * @throws SQLException if there is a problem accessing the database. */ public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws SQLException { // Start by looking up the value name String name = (String) Hibernate.STRING.nullSafeGet(rs, names[0]); if (name == null) { return null; } // Then find the corresponding enumeration value try { return SourceMedia.valueOf(name); } catch (IllegalArgumentException e) { throw new HibernateException("Bad SourceMedia value: " + name, e); } } /** * Write an instance of the mapped class to a {@link PreparedStatement}, * handling null values. * * @param st a JDBC prepared statement. * @param value the SourceMedia value to write. * @param index the parameter index within the prepared statement at which * this value is to be written. * @throws SQLException if there is a problem accessing the database. */ public void nullSafeSet(PreparedStatement st, Object value, int index) throws SQLException { String name = null; if (value != null) name = ((SourceMedia)value).toString(); Hibernate.STRING.nullSafeSet(st, name, index); } /** * Reconstruct an object from the cacheable representation. At the very least this * method should perform a deep copy if the type is mutable. (optional operation) * * @param cached the object to be cached * @param owner the owner of the cached object * @return a reconstructed object from the cachable representation */ public Object assemble(Serializable cached, Object owner) { return cached; } /** * Transform the object into its cacheable representation. At the very least this * method should perform a deep copy if the type is mutable. That may not be enough * for some implementations, however; for example, associations must be cached as * identifier values. (optional operation) * * @param value the object to be cached * @return a cachable representation of the object */ public Serializable disassemble(Object value) { return (Serializable)value; } /** * Get a hashcode for an instance, consistent with persistence "equality". * @param x the instance whose hashcode is desired. */ public int hashCode(Object x) { return x.hashCode(); } /** * During merge, replace the existing (target) value in the entity we are merging to * with a new (original) value from the detached entity we are merging. For immutable * objects, or null values, it is safe to simply return the first parameter. For * mutable objects, it is safe to return a copy of the first parameter. For objects * with component values, it might make sense to recursively replace component values. * * @param original the value from the detached entity being merged * @param target the value in the managed entity * @return the value to be merged */ public Object replace(Object original, Object target, Object owner) throws HibernateException { return original; } }
Although it may look daunting, don’t panic. All of the methods in
this class are required by the UserType
interface.
Our implementations are quite brief and straightforward, as befits the
simple mapping we’ve undertaken. The first three methods don’t need any
discussion beyond what’s in the JavaDoc and inline comments. Here are some
notes about the interesting bits:
The sqlTypes()
method reports
to Hibernate the number of columns that will be needed to
store values managed by this custom type and the SQL
types of those columns. We indicate that our type uses a single
VARCHAR
column.
Since the API specifies that this
information is to be returned as an array, safe coding practices
dictate that we create and return a new array on each call, to
protect against malicious or buggy code that might manipulate the
contents of the array. (Java has no support for immutable arrays. It
would have been preferable if the UserType
interface declared this method to return a Collection
or
List
, since these can be immutable.)
In nullSafeGet()
we translate
database results into the corresponding MediaSource
enumeration value. Since
we know we stored the value as a string in the database, we can
delegate the actual retrieval to Hibernate’s utility method for
loading strings from database results. You’ll be able to do something
like this in most cases.
Then it’s just a matter of using the enumeration’s own instance lookup capability.
HibernateException
is a RuntimeException
thrown by
Hibernate when there is a problem performing a mapped operation. We’re
“freeloading” on the exception here since our issue arguably relates
to mapping. If we wanted to get fancy we could define our own
exception type to provide more details, but it might still make sense
to have that extend HibernateException
,
especially when working in the context of an abstraction framework
like Spring, as we’ll explore in Chapter 13.
Mapping the other direction is handled by nullSafeSet()
. Once again
we can rely on built-in features of the Java 5 enum
mechanism to translate from a
SourceMedia
instance to
its name, and then use Hibernate’s utilities to store this string in
the database.
In all the methods dealing with values, it’s important to
write your code in a way that will not crash if any of the arguments
are null
, as they often will be.
The “nullSafe
” prefix in some
method names is a reminder of this, but even the
equals()
method must be used
carefully. Blindly delegating to x.equals(y)
would blow
up if x
is null
.
And the rest of the methods are trivial implementations of the interface, because when dealing with immutable singletons, as enumeration values are, the potentially tricky facets of persistence management are avoided.
All right, we’ve created a custom type persistence handler, and it wasn’t so bad! Now it’s time to actually use it to persist our enumeration data.
That’s it. No, really!
This is actually almost embarrassingly easy. Once we’ve got the
value class, SourceMedia
,
and the persistence manager, SourceMediaType
, in
place, all we need to do is use our custom persistence manager class
rather than the raw value type, whenever we want to map it.
We’ll walk through an example of using our source media enumeration to illustrate this. But before diving into it, let’s step back for a minute and think about generalizing our implementation for use in a bigger project.
…if there is more than one enumeration that you want to persist?
If you’ve thought about it, you probably realized there was essentially
nothing in Example 6-2 that was deeply tied to our
SourceMedia
enumeration. There are only a handful
of places (even fewer if you discount the JavaDoc) where the type is
even mentioned. Wouldn’t it be pretty easy to parameterize this and
support any enum
type with a single
implementation?
Indeed, that’s pretty easy, and it would have been nice if a simple but flexible implementation had been built into Hibernate. There are a number that you can choose from on the Hibernate wiki (way more than you may want to sort through, on several different pages), so perhaps we should adopt Gavin King’s Enhanced UserType found on the Java 5 EnumUserType page as our unofficial “official choice” since he’s the author of so much of Hibernate itself. It seems fairly full-featured without going overboard. At least consider comparing it to Example 6-2 to see the kind of work needed to generalize our solution.
I can hear some exclamations of “it’s about time!”
But now, back to concrete examples—let’s see how to actually use our enumeration!
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.
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.
... <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….
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.
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.
<!-- 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">
We create a target named usertypes
for compiling just the user
type definitions, right before the existing codegen
target. These don’t refer to any
generated classes, so they can be compiled before the codegen
target is run. The easiest way to
select them is to take advantage of the fact that they reside in the
com.oreilly.hh package, and the naming convention
we’re using here (which Hibernate itself uses for its type mapping
classes), in which their filenames end in “Type.java” (e.g., SourceMediaType.java, and later in
this chapter, StereoVolumeType.java).
If you don’t have such a convention, you can explicitly list all the files here, or put them in their own separate package. This approach happens to work well for our needs.
Then we update the codegen
target to list usertypes
as its
dependency. This will ensure that the custom type mappings needed by
the code generation task are always compiled and available before it
runs. (We no longer need to list prepare
as a dependency here, since
it is now a dependency of the usertypes
target.)
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.
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.
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.
... // 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.
... 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).
/**
* 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.
Recall that in our Track
object we have
a property that determines our preferred playback volume for
the track. Suppose we’d like the jukebox system to be able to adjust the
balance of tracks for playback, rather
than just their volume. To accomplish this we’d need to store separate
volumes for the left and right channels. The quick solution would be to
edit the Track
mapping to store these
as separate mapped properties.
If we’re serious about object-oriented architecture, we might want
to encapsulate these two values into a StereoVolume
class. This class could then simply be mapped as a composite-element
, as the
AlbumTrack
component was in Example 5-4. This is still
fairly straightforward.
There is a drawback, however, to this simple approach. It’s likely
we will discover other places in our system where we want to represent
StereoVolume
values. If we build a playlist
mechanism that can override a track’s default playback options, and also
want to be able to assign volume control to entire albums, suddenly we
have to recreate the composite mapping in several places, and we might not
do it consistently everywhere (this is more likely to be an issue with a
more complex compound type, but you get the idea). The Hibernate reference
documentation says that it’s a good practice to use a composite user type
in situations like this, and I agree.
Let’s start by defining the StereoVolume
class. There’s no reason for this to be an entity (to have its own
existence independent of some other persistent object), so we’ll write
it as an ordinary (and rather simple) Java object. See Example 6-8.
The JavaDoc in this example has been compressed to take less space. We’re trusting you not to do this in real projects. The downloadable version is more complete.
package com.oreilly.hh; import java.io.Serializable; /** * A simple structure encapsulating a stereo volume level. */ public class StereoVolume implements Serializable { /** The minimum legal volume level. */ public static final short MINIMUM = 0; /** The maximum legal volume level. */ public static final short MAXIMUM = 100; /** Stores the volume of the left channel. */ private short left; /** Stores the volume of the right channel. */ private short right; /** Default constructor sets full volume in both channels. */ public StereoVolume() { this(MAXIMUM, MAXIMUM); } /** Constructor that establishes specific volume levels. */ public StereoVolume(short left, short right) { setLeft(left); setRight(right); } /** * Helper method to make sure a volume value is legal. * @param volume the level that is being set. * @throws IllegalArgumentException if it is out of range. */ private void checkVolume(short volume) { if (volume < MINIMUM) { throw new IllegalArgumentException("volume cannot be less than " + MINIMUM); } if (volume > MAXIMUM) { throw new IllegalArgumentException("volume cannot be more than " + MAXIMUM); } } /** Set the volume of the left channel. */ public void setLeft(short volume) { checkVolume(volume); left = volume; } /** Set the volume of the right channel. */ public void setRight(short volume) { checkVolume(volume); right = volume; } /** Get the volume of the left channel */ public short getLeft() { return left; } /** Get the volume of the right channel. */ public short getRight() { return right; } /** Format a readable version of the volume levels, for debugging. */ public String toString() { return "Volume[left=" + left + ", right=" + right + ']'; } /** * Compare whether another object is equal to this one. * @param obj the object to be compared. * @return true if obj is also a StereoVolume instance, and represents * the same volume levels. */ public boolean equals(Object obj) { if (obj instanceof StereoVolume) { StereoVolume other = (StereoVolume)obj; return other.getLeft() == getLeft() && other.getRight() == getRight(); } return false; // It wasn't a StereoVolume } /** * Returns a hash code value for the StereoVolume. This method must be * consistent with the {@link #equals} method. */ public int hashCode() { return (int)getLeft() * MAXIMUM * 10 + getRight(); } }
To let us persist this as a composite type, rather than defining
it as a nested compound object each time we use it, we build a custom
user type to manage its persistence. A lot of what we need to provide in
our custom type is the same as what we put in SourceMediaType
(Example 6-2, shown
earlier in this chapter). We’ll focus on the new and interesting stuff.
Example 6-9
shows one way to persist StereoVolume
as a
composite user type.
package com.oreilly.hh; import java.io.Serializable; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import org.hibernate.Hibernate; import org.hibernate.engine.SessionImplementor; import org.hibernate.type.Type; import org.hibernate.usertype.CompositeUserType; /** * Manages persistence for the {@link StereoVolume} composite type. */ public class StereoVolumeType implements CompositeUserType { /** * Get the names of the properties that make up this composite type, and * that may be used in a query involving it. */ public String[] getPropertyNames() { // Allocate a new response each time, because arrays are mutable return new String[] { "left", "right" }; } /** * Get the types associated with the properties that make up this composite * type. * * @return the types of the parameters reported by {@link #getPropertynames}, * in the same order. */ public Type[] getPropertyTypes() { return new Type[] { Hibernate.SHORT, Hibernate.SHORT }; } /** * Look up the value of one of the properties making up this composite type. * * @param component a {@link StereoVolume} instance being managed. * @param property the index of the desired property. * @return the corresponding value. * @see #getPropertyNames */ public Object getPropertyValue(Object component, int property) { StereoVolume volume = (StereoVolume)component; short result; switch (property) { case 0: result = volume.getLeft(); break; case 1: result = volume.getRight(); break; default: throw new IllegalArgumentException("unknown property: " + property); } return new Short(result); } /** * Set the value of one of the properties making up this composite type. * * @param component a {@link StereoVolume} instance being managed. * @param property the index of the desired property. * @object value the new value to be established. * @see #getPropertyNames */ public void setPropertyValue(Object component, int property, Object value) { StereoVolume volume = (StereoVolume)component; short newLevel = ((Short)value).shortValue(); switch (property) { case 0: volume.setLeft(newLevel); break; case 1: volume.setRight(newLevel); break; default: throw new IllegalArgumentException("unknown property: " + property); } } /** * Determine the class that is returned by {@link #nullSafeGet}. * * @return {@link StereoVolume}, the actual type returned by * {@link #nullSafeGet}. */ public Class returnedClass() { return StereoVolume.class; } /** * Compare two instances of the class mapped by this type for persistence * "equality". * * @param x first object to be compared. * @param y second object to be compared. * @return <code>true</code> iff both represent the same volume levels. * @throws ClassCastException if x or y isn't a {@link StereoVolume}. */ public boolean equals(Object x, Object y) { if (x == y) { // This is a trivial success return true; } if (x == null || y == null) { // Don't blow up if either is null! return false; } // Now it's safe to delegate to the class' own sense of equality return ((StereoVolume)x).equals(y); } /** * Return a deep copy of the persistent state, stopping at entities and * collections. * * @param value the object whose state is to be copied. * @return a copy representing the same volume levels as the original. * @throws ClassCastException for non {@link StereoVolume} values. */ public Object deepCopy(Object value) { if (value == null) return null; StereoVolume volume = (StereoVolume)value; return new StereoVolume(volume.getLeft(), volume.getRight()); } /** * Indicates whether objects managed by this type are mutable. * * @return <code>true</code>, since {@link StereoVolume} is mutable. */ public boolean isMutable() { return true; } /** * Retrieve an instance of the mapped class from a JDBC {@link ResultSet}. * * @param rs the results from which the instance should be retrieved. * @param names the columns from which the instance should be retrieved. * @param session an extension of the normal Hibernate session interface * that gives you much more access to the internals. * @param owner the entity containing the value being retrieved. * @return the retrieved {@link StereoVolume} value, or <code>null</code>. * @throws SQLException if there is a problem accessing the database. */ public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws SQLException { Short left = (Short)Hibernate.SHORT.nullSafeGet(rs, names[0]); Short right = (Short)Hibernate.SHORT.nullSafeGet(rs, names[1]); if (left == null || right == null) { return null; // We don't have a specified volume for the channels } return new StereoVolume(left.shortValue(), right.shortValue()); } /** * Write an instance of the mapped class to a {@link PreparedStatement}, * handling null values. * * @param st a JDBC prepared statement. * @param value the StereoVolume value to write. * @param index the parameter index within the prepared statement at which * this value is to be written. * @param session an extension of the normal Hibernate session interface * that gives you much more access to the internals. * @throws SQLException if there is a problem accessing the database. */ public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws SQLException { if (value == null) { Hibernate.SHORT.nullSafeSet(st, null, index); Hibernate.SHORT.nullSafeSet(st, null, index + 1); } else { StereoVolume vol = (StereoVolume)value; Hibernate.SHORT.nullSafeSet(st, new Short(vol.getLeft()), index); Hibernate.SHORT.nullSafeSet(st, new Short(vol.getRight()), index + 1); } } /** * Reconstitute a working instance of the managed class from the cache. * * @param cached the serializable version that was in the cache. * @param session an extension of the normal Hibernate session interface * that gives you much more access to the internals. * @param owner the entity containing the value being retrieved. * @return a copy of the value as a {@link StereoVolume} instance. */ public Object assemble(Serializable cached, SessionImplementor session, Object owner) { // Our value type happens to be serializable, so we have an easy out. return deepCopy(cached); } /** * Translate an instance of the managed class into a serializable form to be * stored in the cache. * * @param session an extension of the normal Hibernate session interface * that gives you much more access to the internals. * @param value the StereoVolume value to be cached. * @return a serializable copy of the value. */ public Serializable disassemble(Object value, SessionImplementor session) { return (Serializable)deepCopy(value); } /** * Get a hashcode for the instance, consistent with persistence "equality" */ public int hashCode(Object x) { return x.hashCode(); // Can delegate to our well-behaved object } /** * During merge, replace the existing (target) value in the entity we are * merging to with a new (original) value from the detached entity we are * merging. For immutable objects, or null values, it is safe to simply * return the first parameter. For mutable objects, it is safe to return a * copy of the first parameter. However, since composite user types often * define component values, it might make sense to recursively replace * component values in the target object. * * @param original value being merged from. * @param target value being merged to. * @param session the hibernate session into which the merge is happening. * @param owner the containing entity. * @return an independent value that can safely be used in the new context. */ public Object replace(Object original, Object target, SessionImplementor session, Object owner) { return deepCopy(original); } }
Due to the getPropertyNames()
and getPropertyTypes()
methods,
Hibernate knows the “pieces” that make up the composite type. These
are the values that are available when you write
HQL queries using the type. In our case they
correspond to the properties of the actual
StereoVolume
class we’re persisting, but that
isn’t required. This is our opportunity, for example, to provide a
friendly property interface to some legacy object that wasn’t
designed for persistence at all.
The translation between the virtual properties provided by
the composite user type and the real data on which they
are based is handled by the getPropertyValue()
and
setPropertyValue()
methods. In
essence, Hibernate hands us an instance of the type we’re supposed
to manage, about which it makes no assumptions at all, and says,
“hey, give me the second property,” or, “set the first property to
this value.” You can see how this lets us do any work needed to add
a property interface to old or third-party code. In this case, since
we don’t actually need that power, the hoops we need to jump through
to pass the property manipulation on to the underlying
StereoVolume
class are just
boilerplate.
The next lengthy stretch of code consists of methods we’ve
seen before in Example 6-2. Some of the differences in this
version are interesting. Most of the changes have to do with the
fact that, unlike SourceMedia
, our
StereoVolume
class is mutable—it contains
values that can be changed. So we have to come up with full
implementations for some methods we finessed last time.
We need to provide a meaningful way of comparing instances in
equals()
,
and of making independent copies in
deepCopy()
.
The actual persistence methods, nullSafeGet()
and
nullSafeSet()
, are quite similar to
Example 6-2, with one difference we didn’t need to
exploit. They both have a SessionImplementor
parameter, which gives you some really deep access to the gears and
pulleys that make Hibernate work. This is only needed for truly
complex persistence challenges, and it is well outside the scope of
this book. If you need to use SessionImplementor
methods,
you’re doing something quite tricky, and you must have a profound
understanding of the architecture of Hibernate. You’re essentially
writing an extension to the system, and you probably need to study
the source code to develop the requisite level of expertise.
The assemble()
and disassemble()
methods
allow custom types to support caching of values that aren’t already
Serializable
. They give our persistence
helper a place to copy any important values into another object that
is capable of being serialized, using any means necessary. Since it
was trivial to make StereoVolume
serializable
in the first place, we don’t need this flexibility either. Our
implementation can just make copies of the serializable
StereoVolume
instances for storing in the
cache. (We make copies because, again, our data class is mutable,
and it wouldn’t do to have cached values mysteriously
changing.)
The hashCode()
method was added
in Hibernate 3, requiring changes to CompositeUserType
implementations,
but it helps with efficiency. In our case, we have an object that
already implements this method to which we can delegate, but, again,
if we were wrapping some crufty legacy data structures, this is an
opportunity to put a nice Java wrapper on them.
Finally, the replace()
method
is another new Hibernate 3 requirement. Again, since we
have a handy object to copy, we can take an easy way out.
Alternatively, we could have manually copied all the nested property
values from the original object to the target object.
That may seem like a lot of work for a simple value class, but it shows you all you’ll need to know when you have more complex values to model.
All right, we’ve created this beast, how do we use it? Example 6-10
shows how to enhance the volume
property in the Track
mapping document to use the
new composite type. Let’s also take this opportunity to add it to
Track
’s toString()
method so we can see it in test output.
... <property name="volume" type="com.oreilly.hh.StereoVolumeType
"> <meta attribute="field-description">How loud to play the track</meta><meta attribute="use-in-tostring">true</meta>
<column name="VOL_LEFT"/>
<column name="VOL_RIGHT"/>
</property> ...
Notice again that we supply the name of our custom user type, responsible for managing persistence, rather than the raw type that it is managing. This is just like Example 6-3. Also, our composite type uses two columns to store its data, so we need to supply two column names here.
Now when we regenerate the Java source for Track
by running ant
codegen, we get the results shown in Example 6-11.
... /** * How loud to play the track */ privateStereoVolume
volume; ... public Track(String title, String filePath, Date playTime, Set<Artist> artists, Date added,StereoVolume
volume, SourceMedia sourceMedia, Set<String> comments) { ... } ... /** * How loud to play the track */ publicStereoVolume
getVolume() { return this.volume; } public void setVolume(StereoVolume
volume) { this.volume = volume; } ... public String toString() { StringBuffer buffer = new StringBuffer(); buffer.append(getClass().getName()).append("@").append(Integer.toHexString (hashCode())).append(" ["); buffer.append("title").append("='").append(getTitle()).append("' ");buffer.append("volume").append("='").append(getVolume()).append("' ");
buffer.append("sourceMedia").append("='").append(getSourceMedia()).append( "' "); buffer.append("]"); return buffer.toString(); } ...
At this point we are ready to run ant schema to recreate the database tables. Example 6-12 shows the relevant output.
...
[hibernatetool] create table TRACK (TRACK_ID integer generated by default as
identity (start with 1), TITLE varchar(255) not null,
filePath varchar(255) not null, playTime time, added date,
VOL_LEFT smallint, VOL_RIGHT smallint,
sourceMedia varchar(255),
primary key (TRACK_ID));
...
Let’s beef up the data creation test so it can work with the new
Track
structure. Example 6-13
shows the kind of changes we need.
... // Create some data and persist it tx = session.beginTransaction();StereoVolume fullVolume = new StereoVolume();
Track track = new Track("Russian Trance", "vol2/album610/track02.mp3", Time.valueOf("00:03:30"), new HashSet<Artist>(), new Date(),fullVolume
, SourceMedia.CD, new HashSet<String>()); addTrackArtist(track, getArtist("PPK", true, session)); session.save(track); ...// The other tracks created use fullVolume too, until...
... track = new Track("Test Tone 1", "vol2/singles/test01.mp3", Time.valueOf("00:00:10"), new HashSet<Artist>(), new Date(),new StereoVolume((short)50, (short)75)
, null, new HashSet<String>()); track.getComments().add("Pink noise to test equalization"); session.save(track); ...
Now if we execute ant ctest and look at the results with ant db, we’ll find values like those shown in Figure 6-2.
We only need to make the single change, shown in Example 6-14, to
AlbumTest
to make it compatible with this new
Track
format.
...
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(), new StereoVolume()
, SourceMedia.CD,
new HashSet<String>());
...
This lets us run ant atest and
see the stereo volume information, shown by the new version of
Track
’s toString()
method in Example 6-15.
[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'volume='Volume[left=100, right=100]'
sourceMedia='CD' ]' ], c om.oreilly.hh.data.AlbumTrack@aa8eb7 [track='com.oreilly.hh.data.Track@7fc8a0 [t itle='In a Manner of Speaking'volume='Volume[left=100, right=100]'
sourceMedia= 'CD' ]' ], com.oreilly.hh.data.AlbumTrack@4cadc4 [track='com.oreilly.hh.data.Tra ck@243618 [title='Smile in the Crowd'volume='Volume[left=100, right=100]'
sourc eMedia='CD' ]' ], com.oreilly.hh.data.AlbumTrack@5b644b [track='com.oreilly.hh.d ata.Track@157e43 [title='Gone'volume='Volume[left=100, right=100]'
sourceMedia= 'CD' ]' ], com.oreilly.hh.data.AlbumTrack@1483a0 [track='com.oreilly.hh.data.Tra ck@cdae24 [title='Never Turn Your Back on Mother Earth'volume='Volume[left=100,
right=100]'
sourceMedia='CD' ]' ], com.oreilly.hh.data.AlbumTrack@63dc28 [track ='com.oreilly.hh.data.Track@ae511 [title='Motherless Child' volume='Volume[left= 100, right=100]' sourceMedia='CD' ]' ]]' ]
Well, we may have gone into more depth about creating custom types than you wanted right now, but someday you might come back and mine this example for the exact nugget you’re looking for. In the meantime, let’s change gears and look at something new, simple, and completely different. Chapter 7 shows an alternative to using XML mapping documents at all, and Chapter 8 introduces criteria queries, a unique and very programmer-friendly capability in Hibernate.
…other fancy tricks in mapping custom types? All right, if this
hasn’t been too much information, you can explore the other interfaces
in the org.hibernate.usertype package, which
include EnhancedUserType
(which lets your
custom type act as an entity’s id
and do other tricks)
and ParameterizedUserType
(that can be configured
to support mapping multiple different types) among others. The reusable
mappings for Java 5 enum
s discussed
on the Java 5
EnumUserType page of the Hibernate wiki illustrate
good uses of both of these, but they go beyond the scope of
this book.