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: net.sf.hibernate.UserType or net.sf.hibernate.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. In Chapter 6 we saw how to use Hibernate's built-in enumeration support to persist a typesafe enumeration to an integer column, and we had to work around the fact that many object-oriented enumerations have no natural integer representation. While we can hope that Java 1.5 will allow Hibernate to resolve this tension in a universal way, we don't have to wait for that to happen, nor do we necessarily have to make the kind of compromises we did in Example 6-2. We can define our own custom value type that persists the SourceMedia class on its own terms. Later in the chapter we'll look at a more complex example involving multiple properties and columns.
We'll work with the verson of SourceMedia.java shown in Example 6-1. Our custom type will allow this class to be persisted without any changes from its original form. In other words, the design of our data classes can be dictated by the needs and semantics of the application alone, and we can move the persistence support into a separate class focused on that sole purpose. This is a much better division of labor.
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 is 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 7-1.
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.
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. 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 slightly 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.
Mapping the other direction is handled by nullSafeSet(). Once again we can rely on built-in features of the enumeration to translate from a MediaSource 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 careful. Blindly delegating to x.equals(y) would blow up if x is null.