Chapter 6. Custom Value Types

Defining a User Type

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.

Note

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.

How do I do that?

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.

Defining a Persistent Enumerated Type

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.

Note

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

Example 6-1. SourceMedia.java, our initial typesafe enumeration
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.

Note

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?

Using a Custom Type Mapping

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.

Example 6-2. SourceMediaType.java, our custom type mapping handler
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() { 1
        // 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 2
    {
        // 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); 3
        }
        catch (IllegalArgumentException e) {
            throw new HibernateException("Bad SourceMedia value: " + name, e); 4
        }
    }

    /**
     * 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 5
    {
        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:

1

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.

Warning

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

2

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.

3

Then it’s just a matter of using the enumeration’s own instance lookup capability.

4

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.

5

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.

Tip

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.

How do I do that?

Note

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.

What about…

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

Note

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!

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.

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

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" 1
          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" 2
          description="Generate Java source from the O/R mapping files">
1

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.

2

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

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.

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.

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

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.

Building a Composite User Type

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.

How do I do that?

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.

Note

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.

Example 6-8. StereoVolume.java, a value class representing a stereo volume level
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() { 1
        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) { 2
        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) { 3
        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();
    }
}
1

Since we want to be able to persist this with Hibernate, we provide a default constructor…

2

…and property accessors.

3

Correct support for the Java equals() and hashCode() contracts is also important, since this is a mutable value object.

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.

Example 6-9. StereoVolumeType.java, a composite user type to persist StereoVolume
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() { 1
        // 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) { 2
        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) { 3
        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) { 4
        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, 5
                              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, 6
                           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) { 7
        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, 8
                          SessionImplementor session, Object owner) {
        return deepCopy(original);
    }
}
1

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.

2

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.

3

We need to provide a meaningful way of comparing instances in equals(),

4

and of making independent copies in deepCopy().

5

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.

6

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

7

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.

8

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.

Note

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.

Example 6-10. Changes to Track.hbm.xml to use StereoVolume
...
<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.

Example 6-11. Changes to the generated Track.java source
...

/**
 * How loud to play the track
 */
private StereoVolume 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
 */
public StereoVolume 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.

Example 6-12. Creation of the track schema from the new mapping
...
[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.

Example 6-13. Changes required to CreateTest.java to test stereo volumes
...
// 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.

Stereo volume information in the TRACK table
Figure 6-2. Stereo volume information in the TRACK table

We only need to make the single change, shown in Example 6-14, to AlbumTest to make it compatible with this new Track format.

Example 6-14. Change to AlbumTest.java to support stereo track volumes
...
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.

Example 6-15. An album with stereo track information
     [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.

What about…

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

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset