Hibernate supports a wealth of Java types, be they simple values or 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 requires the ability to tweak the type system is when you have a single property value that needs to get 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.
7.1 Defining a User Type
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.
7.1.1 How do I do that?
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.
Example 7-1. 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 net.sf.hibernate.UserType;
import net.sf.hibernate.Hibernate;
import net.sf.hibernate.HibernateException;
import net.sf.hibernate.type.Type;
/**
* 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.
* @throws ClassCastException for non {@link SourceMedia} values.
*/
public Object deepCopy(Object value) {
return (SourceMedia)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 = {
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 HibernateException if there is a problem performing the mapping.
* @throws SQLException if there is a problem accessing the database.
*/
public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
throws HibernateException, 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.getInstanceByName(name);
}
catch (java.util.NoSuchElementException 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 HibernateException if there is a problem performing the mapping.
* @throws SQLException if there is a problem accessing the database.
*/
public void nullSafeSet(PreparedStatement st, Object value, int index)
throws HibernateException, SQLException
{
String name = null;
if (value != null)
name = ((SourceMedia)value).getName();
Hibernate.STRING.nullSafeSet(st, name, index);
}
}
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.
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.
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.
7.2 Using a Custom Type Mapping
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 the way we want it.
7.2.1 How do I do that?
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 modify any mapping documents that were previously referring to the raw value type to refer instead to the custom persistence manager.
NOTE
That's it. No, really!
In our case, that means we change the mapping for the mediaSource property in Track.hbm.xml so it looks like Example 7-2 rather than Example 6-3.
Example 7-2. Custom type mapping for the sourceMedia 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>
At this point, running ant schema will rebuild the database schema, changing the SOURCEMEDIA column in the TRACK table from integer to VARCHAR (as specified by SourceMediaType's sqlTypes() method).
Thanks to the beauty of letting the object/relational mapping layer handle the details of how data is stored and retrieved, we don't need to change any aspect of the example or test code that we were using in Chapter 6. You can run ant ctest to create sample data. It will run with no complaint. If you fire up ant db to look at the way it's stored, you'll find that our goal of storing semantically meaningful enumeration symbols has been achieved, as shown in Figure 7-1.
Figure 7-1. Nicer source media information in the TRACK table
Getting the data back out works just as well. Running ant qtest produces output that is identical to what we obtained when we were using Hibernate's built-in, numeric enumeration support. Try it yourself, or compare Example 7-3 with Example 6-5.
Example 7-3. You can't tell the difference at the application layer
...
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 VHS
Videocassette Tape
[java] Track: "Gravity's Angel" (Laurie Anderson) 00:06:06, from Compact Disc
[java] Track: "Adagio for Strings (Ferry Corsten Remix)" (Ferry Corsten,
William Orbit, Samuel Barber) 00:06:35, from Compact Disc
[java] Track: "Test Tone 1" 00:00:10
[java] Comment: Pink noise to test equalization
...
NOTE
Encapsulation and abstraction are wonderful things, aren't they?
7.2.2 What about...
...More complicated custom type mappings, such as splitting single properties into multiple database columns, or single columns into multiple properties? As noted earlier, your persistence handler class needs to implement CompositeUserType instead of UserType to provide this service. That interface adds only a few more methods for you to flesh out, and they deal primarily with teaching Hibernate about the synthetic properties you want to make available in queries, and providing ways for it to get and set the values of these properties. Let's look at an example!
7.3 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 we did with the AlbumTrack component in lines 38-45 of Example 5-4. This is still fairly straightforward.
There is a drawback 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.
7.3.1 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. Example 7-4 shows the source.
NOTE
The JavaDoc in this example has been compressed to take less space. I'm trusting you not to do this in real projects... the downloadable version is more complete.
Example 7-4. StereoVolume.java, which is a value class representing a stereo volume level
1 package com.oreilly.hh;
2
3 import java.io.Serializable;
4
5 /**
6 * A simple structure encapsulating a stereo volume level.
7 */
8 public class StereoVolume implements Serializable {
9
10 /** The minimum legal volume level. */
11 public static final short MINIMUM = 0;
12
13 /** The maximum legal volume level. */
14 public static final short MAXIMUM = 100;
15
16 /** Stores the volume of the left channel. */
17 private short left;
18
19 /** Stores the volume of the right channel. */
20 private short right;
21
22 /** Default constructor sets full volume in both channels. */
23 public StereoVolume() {
24 this(MAXIMUM, MAXIMUM);
25 }
26
27 /** Constructor that establishes specific volume levels. */
28 public StereoVolume(short left, short right) {
29 setLeft(left);
30 setRight(right);
31 }
32
33 /**
34 * Helper method to make sure a volume value is legal.
35 * @param volume the level that is being set.
36 * @throws IllegalArgumentException if it is out of range.
37 */
38 private void checkVolume(short volume) {
39 if (volume < MINIMUM) {
40 throw new IllegalArgumentException("volume cannot be less than " +
41 MINIMUM);
42 }
43 if (volume > MAXIMUM) {
44 throw new IllegalArgumentException("volume cannot be more than " +
45 MAXIMUM);
46 }
47 }
48
49 /** Set the volume of the left channel. */
50 public void setLeft(short volume) {
51 checkVolume(volume);
52 left = volume;
53 }
54
55 /** Set the volume of the right channel. */
56 public void setRight(short volume) {
57 checkVolume(volume);
58 right = volume;
59 }
60
61 /** Get the volume of the left channel */
62 public short getLeft() {
63 return left;
64 }
65
66 /** Get the volume of the right channel. */
67 public short getRight() {
68 return right;
69 }
70
71 /** Format a readable version of the volume levels. */
72 public String toString() {
73 return "Volume[left=" + left + ", right=" + right + ']';
74 }
75
76 /**
77 * Compare whether another object is equal to this one.
78 * @param obj the object to be compared.
79 * @return true if obj is also a StereoVolume instance, and represents
80 * the same volume levels.
81 */
82 public boolean equals(Object obj) {
83 if (obj instanceof StereoVolume) {
84 StereoVolume other = (StereoVolume)obj;
85 return other.getLeft() == getLeft() &&
86 other.getRight() == getRight();
87 }
88 return false; // It wasn't a StereoVolume
89 }
90
91 /**
92 * Returns a hash code value for the StereoVolume. This method must be
93 * consistent with the {@link #equals} method.
94 */
95 public int hashCode() {
96 return (int)getLeft() * MAXIMUM * 10 + getRight();
97 }
98 }
Since we want to be able to persist this with Hibernate, we provide a default constructor (lines 22-25) and property accessors (lines 49-69). Correct support for the Java equals() and hashCode() contracts is also important, since this is a mutable value object (lines 76 to the end).
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 7-1). We'll focus discussion on the new and interesting stuff. Example 7-5 shows one way to persist StereoVolume as a composite user type.
Example 7-5. StereoVolumeType.java, which is a composite user type to persist StereoVolume
1 package com.oreilly.hh;
2
3 import java.io.Serializable;
4 import java.sql.PreparedStatement;
5 import java.sql.ResultSet;
6 import java.sql.SQLException;
7 import java.sql.Types;
8
9 import net.sf.hibernate.CompositeUserType;
10 import net.sf.hibernate.Hibernate;
11 import net.sf.hibernate.HibernateException;
12 import net.sf.hibernate.engine.SessionImplementor;
13 import net.sf.hibernate.type.Type;
14
15 /**
16 * Manages persistence for the {@link StereoVolume} composite type.
17 */
18 public class StereoVolumeType implements CompositeUserType {
19
20 /**
21 * Get the names of the properties that make up this composite type,
22 * and that may be used in a query involving it.
23 */
24 public String[] getPropertyNames() {
25 // Allocate a new response each time, because arrays are mutable
26 return new String[] { "left", "right" };
27 }
28
29 /**
30 * Get the types associated with the properties that make up this
31 * composite type.
32 *
33 * @return the types of the parameters reported by
34 * {@link #getPropertynames}, in the same order.
35 */
36 public Type[] getPropertyTypes() {
37 return new Type[] { Hibernate.SHORT, Hibernate.SHORT };
38 }
39
40 /**
41 * Look up the value of one of the properties making up this composite
42 * type.
43 *
44 * @param component a {@link StereoVolume} instance being managed.
45 * @param property the index of the desired property.
46 * @return the corresponding value.
47 * @see #getPropertyNames
48 */
49 public Object getPropertyValue(Object component, int property) {
50 StereoVolume volume = (StereoVolume)component;
51 short result;
52
53 switch (property) {
54
55 case 0:
56 result = volume.getLeft();
57 break;
58
59 case 1:
60 result = volume.getRight();
61 break;
62
63 default:
64 throw new IllegalArgumentException("unknown property: " +
65 property);
66 }
67
68 return new Short(result);
69 }
70
71 /**
72 * Set the value of one of the properties making up this composite
73 * type.
74 *
75 * @param component a {@link StereoVolume} instance being managed.
76 * @param property the index of the desired property.
77 * @object value the new value to be established.
78 * @see #getPropertyNames
79 */
80 public void setPropertyValue(Object component, int property, Object value)
81 {
82 StereoVolume volume = (StereoVolume)component;
83 short newLevel = ((Short)value).shortValue();
84 switch (property) {
85
86 case 0:
87 volume.setLeft(newLevel);
88 break;
89
90 case 1:
91 volume.setRight(newLevel);
92 break;
93
94 default:
95 throw new IllegalArgumentException("unknown property: " +
96 property);
97 }
98 }
99
100 /**
101 * Determine the class that is returned by {@link #nullSafeGet}.
102 *
103 * @return {@link StereoVolume}, the actual type returned
104 * by {@link #nullSafeGet}.
105 */
106 public Class returnedClass() {
107 return StereoVolume.class;
108 }
109
110 /**
111 * Compare two instances of the class mapped by this type for persistence
112 * "equality".
113 *
114 * @param x first object to be compared.
115 * @param y second object to be compared.
116 * @return <code>true</code> iff both represent the same volume levels.
117 * @throws ClassCastException if x or y isn't a {@link StereoVolume}.
118 */
119 public boolean equals(Object x, Object y) {
120 if (x == y) { // This is a trivial success
121 return true;
122 }
123 if (x == null || y == null) { // Don't blow up if either is null!
124 return false;
125 }
126 // Now it's safe to delegate to the class' own sense of equality
127 return ((StereoVolume)x).equals(y);
128 }
129
130 /**
131 * Return a deep copy of the persistent state, stopping at
132 * entities and collections.
133 *
134 * @param value the object whose state is to be copied.
135 * @return the same object, since enumeration instances are singletons.
136 * @throws ClassCastException for non {@link StereoVolume} values.
137 */
138 public Object deepCopy(Object value) {
139 if (value == null) return null;
140 StereoVolume volume = (StereoVolume)value;
141 return new StereoVolume(volume.getLeft(), volume.getRight());
142 }
143
144 /**
145 * Indicates whether objects managed by this type are mutable.
146 *
147 * @return <code>true</code>, since {@link StereoVolume} is mutable.
148 */
149 public boolean isMutable() {
150 return true;
151 }
152
153 /**
154 * Retrieve an instance of the mapped class from a JDBC {@link ResultSet}.
155 *
156 * @param rs the results from which the instance should be retrieved.
157 * @param names the columns from which the instance should be retrieved.
158 * @param session, an extension of the normal Hibernate session interface
159 * that gives you much more access to the internals.
160 * @param owner the entity containing the value being retrieved.
161 * @return the retrieved {@link StereoVolume} value, or <code>null</code>.
162 * @throws HibernateException if there is a problem performing the mapping.
163 * @throws SQLException if there is a problem accessing the database.
164 */
165 public Object nullSafeGet(ResultSet rs, String[] names,
166 SessionImplementor session, Object owner)
167 throws HibernateException, SQLException
168 {
169 Short left = (Short) Hibernate.SHORT.nullSafeGet(rs, names[0]);
170 Short right = (Short) Hibernate.SHORT.nullSafeGet(rs, names[1]);
171
172 if (left == null || right == null) {
173 return null; // We don't have a specified volume for the channels
174 }
175
176 return new StereoVolume(left.shortValue(), right.shortValue());
177 }
178
179 /**
180 * Write an instance of the mapped class to a {@link PreparedStatement},
181 * handling null values.
182 *
183 * @param st a JDBC prepared statement.
184 * @param value the StereoVolume value to write.
185 * @param index the parameter index within the prepared statement at which
186 * this value is to be written.
187 * @param session, an extension of the normal Hibernate session interface
188 * that gives you much more access to the internals.
189 * @throws HibernateException if there is a problem performing the mapping.
190 * @throws SQLException if there is a problem accessing the database.
191 */
192 public void nullSafeSet(PreparedStatement st, Object value, int index,
193 SessionImplementor session)
194 throws HibernateException, SQLException
195 {
196 if (value == null) {
197 Hibernate.SHORT.nullSafeSet(st, null, index);
198 Hibernate.SHORT.nullSafeSet(st, null, index + 1);
199 }
200 else {
201 StereoVolume vol = (StereoVolume)value;
202 Hibernate.SHORT.nullSafeSet(st, new Short(vol.getLeft()), index);
203 Hibernate.SHORT.nullSafeSet(st, new Short(vol.getRight()),
204 index + 1);
205 }
206 }
207
208 /**
209 * Reconstitute a working instance of the managed class from the cache.
210 *
211 * @param cached the serializable version that was in the cache.
212 * @param session, an extension of the normal Hibernate session interface
213 * that gives you much more access to the internals.
214 * @param owner the entity containing the value being retrieved.
215 * @return a copy of the value as a {@link StereoVolume} instance.
216 */
217 public Object assemble(Serializable cached, SessionImplementor session,
218 Object owner)
219 {
220 // Our value type happens to be serializable, so we have an easy out.
221 return deepCopy(cached);
222 }
223
224 /**
225 * Translate an instance of the managed class into a serializable form to
226 * be stored in the cache.
227 *
228 * @param session, an extension of the normal Hibernate session interface
229 * that gives you much more access to the internals.
230 * @param value the StereoVolume value to be cached.
231 * @return a serializable copy of the value.
232 */
233 public Serializable disassemble(Object value,
234 SessionImplementor session) {
235 return (Serializable) deepCopy(value);
236 }
237 }
The getPropertyNames() and getPropertyTypes() methods at lines 20 and 29 are how 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 lines 40- 98. 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 7-1. 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: comparing instances in equals() at line 110, and making copies in deepCopy() at line 130.
The actual persistence methods, nullSafeGet() at line 153 and nullSafeSet() at 179, are quite similar to Example 7-1, 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.
Finally, the assemble() method at line 208 and disassemble() at 224 allow custom types to support caching of values that aren't already Serializable. They give our persistence manager 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.)
NOTE
That was a lot of work for a simple value class, but the example is a good starting point for more complicated needs.
All right, we've created this beast, how do we use it? Example 7-6 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 7-6. 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 7-2. 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 7-7.
Example 7-7. Changes to the generated Track.java source
...
/** nullable persistent field */
private com.oreilly.hh.StereoVolume volume;
...
/** full constructor */
public Track(String title, String filePath, Date playTime, Date added, com.
oreilly.hh.StereoVolume volume, com.oreilly.hh.SourceMedia sourceMedia, Set
artists, Set comments) {
...
}
...
/**
* How loud to play the track
*/
public com.oreilly.hh.StereoVolume getVolume() {
return this.volume;
}
public void setVolume(com.oreilly.hh.StereoVolume volume) {
this.volume = volume;
}
...
public String toString() {
return new ToStringBuilder(this)
.append("id", getId())
.append("title", getTitle())
.append("volume", getVolume())
.append("sourceMedia", getSourceMedia())
.toString();
}
...
At this point we are ready to run ant schema to recreate the database tables. Example 7-8 shows the relevant output.
Example 7-8. Creation of the Track schema from the new mapping
...
[schemaexport] create table TRACK (
[schemaexport] TRACK_ID INTEGER NOT NULL IDENTITY,
[schemaexport] title VARCHAR(255) not null,
[schemaexport] filePath VARCHAR(255) not null,
[schemaexport] playTime TIME,
[schemaexport] added DATE,
[schemaexport] VOL_LEFT SMALLINT,
[schemaexport] VOL_RIGHT SMALLINT,
[schemaexport] sourceMedia VARCHAR(255)
[schemaexport] )
...
Let's beef up the data creation test so it can work with the new Track structure. Example 7-9 shows the kind of changes we need.
Example 7-9. 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 Date(),
fullVolume, SourceMedia.CD,
new HashSet(), new HashSet());
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 Date(),
new StereoVolume((short)50, (short)75), null,
new HashSet(), new HashSet());
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 7-2.
Figure 7-2. Stereo volume information in the TRACK table
We only need to make the single change, shown in Example 7-10, to AlbumTest to make it compatible with this new Track format.
Example 7-10. 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)
throws HibernateException
{
Track track = new Track(title, file, length, new Date(),
new StereoVolume(), SourceMedia.CD,
new HashSet(), new HashSet());
...
This lets us run ant atest , and see the stereo volume information shown by the new version of Track's toString() method in Example 7-11.
Example 7-11. An album with stereo track information
atest:
[java] com.oreilly.hh.Album@a49182[id=0,title=Counterfeit e.p.,tracks=[com.
oreilly.hh.AlbumTrack@548719[track=com.oreilly.hh.Track@719d5b[id=<null>,
title=Compulsion,volume=Volume[left=100, right=100],sourceMedia=Compact Disc]],
com.oreilly.hh.AlbumTrack@afebc9[track=com.oreilly.hh.Track@a0fbd6[id=<null>,
title=In a Manner of Speaking,volume=Volume[left=100,
right=100],sourceMedia=Compact Disc]], com.oreilly.hh.
AlbumTrack@f5c8fb[track=com.oreilly.hh.Track@5dfb22[id=<null>,title=Smile in the
Crowd,volume=Volume[left=100, right=100],sourceMedia=Compact Disc]], com.oreilly.
hh.AlbumTrack@128f03[track=com.oreilly.hh.Track@6b2ab7[id=<null>,
title=Gone,volume=Volume[left=100, right=100],sourceMedia=Compact Disc]], com.
oreilly.hh.AlbumTrack@c17a8c[track=com.oreilly.hh.Track@549f0e[id=<null>,
title=Never Turn Your Back on Mother Earth,volume=Volume[left=100,
right=100],sourceMedia=Compact Disc]], com.oreilly.hh.
AlbumTrack@9652dd[track=com.oreilly.hh.Track@1a67fe[id=<null>,title=Motherless
Child,volume=Volume[left=100, right=100],sourceMedia=Compact Disc]]]]
Well, that may have been more in-depth than you wanted right now about creating custom types, 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. The next chapter introduces criteria queries, a unique and very programmer-friendly capability in Hibernate.
NOTE
Phew!
No comments:
Post a Comment