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.
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.
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.
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 an 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.
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.)
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.
... <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 source for Track by running ant codegen, we get the results shown in Example 7-7.
... /** 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.
... [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.
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.
We only need to make the single change, shown in Example 7-10, Change to AlbumTest to make it compatible with this new Track format.
... private static void addAlbumTrack(Album album, String title, String file, Time length, Artist artist, int disc, int positionOnDisc, Session session) 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.
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.
Phew!