Wednesday, 26 September 2012

Persistent Enumerated Types

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 Programming Language Guide (Addison- Wesley). This approach requires a fair amount of boilerplate coding, but it lets you do all kinds of interesting and powerful things. The Java 1.5 specification resuscitates 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.

Regardless of how you implement an enumerated type, you're sometimes going to want to be able to persist such values to a database.


6.1 Defining a Persistent Enumerated Type
NOTE


C-style enumerations still appear too often in Java. Older parts of the Sun API contain many of them.


Hibernate has been around for a while and (at least as of this writing) Java 1.5 isn't yet released, so the support for enumerations in Hibernate can't take advantage of its new enum keyword. Instead, Hibernate lets you define your own typesafe enumeration classes however you like, and it provides a mechanism to help you get them into and out of a database, by translating them to and from small integer values. This is something of a regression to the world of C, but it is useful nonetheless.

In our music database, for example, we might want to add a field to our Track class that tells us the medium from which it was imported.

6.1.1 How do I do that?
The key to adding persistence support for our enumeration is to have it implement Hibernate's PersistentEnum interface. This interface has two methods, toInt() and fromInt(), that Hibernate uses to translate between the enumeration constants and values that represent them in a database.

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;



import java.util.*;

import java.io.Serializable;



/**

* This is a typesafe enumeration that identifies the media on which an

* item in our music database was obtained.

**/

public class SourceMedia implements Serializable {



/** Stores the external name of this instance, by which it can be retrieved. */

private final String name;



/**

* Stores the human-readable description of this instance, by which it is

* identified in the user interface.

*/

private final transient String description;



/**

* Return the external name associated with this instance.

* @return the name by which this instance is identified in code.

**/



public String getName() {

return name;

}



/**

* Return the description associated with this instance.

* @return the human-readable description by which this instance is

* identified in the user interface.

**/

public String getDescription() {

return description;

}



/** Keeps track of all instances by name, for efficient lookup. */

private static final Map instancesByName = new HashMap();



/**

* Constructor is private to prevent instantiation except during class

* loading.

*

* @param name the external name of the message type.

* @param description the human readable description of the message type,

* by which it is presented in the user interface.

*/

private SourceMedia(String name, String description) {

this.name = name;

this.description = description;



// Record this instance in the collection that tracks the enumeration

instancesByName.put(name, this);

}



/** The instance that represents music obtained from cassette tape. */

public static final SourceMedia CASSETTE =

new SourceMedia("cassette", "Audio Cassette Tape");



/** The instance that represents music obtained from vinyl. */

public static final SourceMedia VINYL =

new SourceMedia("vinyl", "Vinyl Record");



/** The instance that represents music obtained from VHS tapes. */

public static final SourceMedia VHS =

new SourceMedia("vhs", "VHS Videocassette Tape");



/** The instance that represents music obtained from a compact disc. */

public static final SourceMedia CD =

new SourceMedia("cd", "Compact Disc");



/** The instance that represents music obtained from a broadcast. */

public static final SourceMedia BROADCAST =

new SourceMedia("broadcast", "Analog Broadcast");



/** The instance that represents music obtained as an Internet download. */

public static final SourceMedia DOWNLOAD =

new SourceMedia("download", "Internet Download");



/** The instance that represents music from a digital audio stream. */

public static final SourceMedia STREAM =

new SourceMedia("stream", "Digital Audio Stream");



/**

* Obtain the collection of all legal enumeration values.

* @return all instances of this typesafe enumeration.

*/

public static Collection getAllValues() {

return Collections.unmodifiableCollection(instancesByName.values());

}



/**

* Look up an instance by name.

*

* @param name the external name of an instance.

* @return the corresponding instance.

* @throws NoSuchElementException if there is no such instance.

*/

public static SourceMedia getInstanceByName(String name) {

SourceMedia result = (SourceMedia)instancesByName.get(name);

if (result == null) {

throw new NoSuchElementException(name);

}

return result;

}



/** Return a string representation of this object. */

public String toString() {

return description;

}



/** Insure that deserialization preserves the signleton property. */

private Object readResolve() {

return getInstanceByName(name);

}

}


To add persistence support for this class, all we need to do is implement the PersistentEnum interface. Unfortunately, this requires us to assign an integer value to each instance, and to provide a way of looking up instances by this integer value. This is the "regression to C" mentioned in the introduction. Most typesafe enumerations with which I've worked have not included such an integer representation, since (as in this example) it was not part of their object-oriented semantics. Still, adding this integer property is not that hard. Example 6-2 shows the revisions we need to make in bold. (To save space, unchanged members and methods and some JavaDoc are omitted from this version of the example; the downloadable version is complete.)

Example 6-2. Changes to SourceMedia.java in order to support persistence using Hibernate
package com.oreilly.hh;



import net.sf.hibernate.PersistentEnum;

import java.util.*;

import java.io.Serializable;



/**

* This is a typesafe enumeration that identifies the media on which an

* item in our music database was obtained.

**/

public class SourceMedia implements PersistentEnum, Serializable {

...

/** Stores the integer value used by Hibernate to persist this instance. */

private final int code;

...

/**

* Return the persistence code associated with this instance, as

* mandated by the {@link PersistentEnum} interface.

*/

public int toInt() {

return code;

}

...

/** Keeps track of all instances by code, for efficient lookup.

private static final Map instancesByCode = new HashMap();



/**

* Constructor is private to prevent instantiation except during class

* loading.

*

* @param name the external name of the message type.

* @param description the human readable description of the message type,

* by which it is presented in the user interface.

* @param code the persistence code by which Hibernate stores the instance.

*/

private SourceMedia(String name, String description, int code) {

this.name = name;

this.description = description;

this.code = code;



// Record this instance in the collections that track the enumeration

instancesByName.put(name, this);

instancesByCode.put(new Integer(code), this);

}

...

public static final SourceMedia CASSETTE =

new SourceMedia("cassette", "Audio Cassette Tape", 0);

...

public static final SourceMedia VINYL =

new SourceMedia("vinyl", "Vinyl Record", 1);

...

public static final SourceMedia VHS =

new SourceMedia("vhs", "VHS Videocassette Tape", 2);

...

public static final SourceMedia CD =

new SourceMedia("cd", "Compact Disc", 3);

...

public static final SourceMedia BROADCAST =

new SourceMedia("broadcast", "Analog Broadcast", 4);

...

public static final SourceMedia DOWNLOAD =

new SourceMedia("download", "Internet Download", 5);

...

public static final SourceMedia STREAM =

new SourceMedia("stream", "Digital Audio Stream", 6);

...

/**

* Look up an instance by code, as specified by the {@link PersistentEnum}

* interface.

*

* @param code the persistence code of an instance.

* @return the corresponding instance.

* @throws NoSuchElementException if there is no such instance.

*/

public static SourceMedia fromInt(int code) {

SourceMedia result =

(SourceMedia)instancesByCode.get(new Integer(code));

if (result == null) {

throw new NoSuchElementException("code=" + code);

}

return result;

}

...

}


An alternative to adding the codes to the constructor arguments is to use a static counter that gets incremented each time a new instance is constructed. Although this is more convenient and concise, it makes it much harder to tell by inspection which code goes with which instance, and it also means you need to be careful to add any new instances to the end of the construction code if you don't want existing values to be rearranged (this is a problem if you've already got values persisted in the database). These are some of the reasons it'd be nicer to avoid the numeric codes completely, and use the symbolic names to represent instances in the database.

NOTE


If you're in too much suspense, rest assured that the next chapter shows a nice way to avoid the need for such numeric codes.


The good news is that once we've got our persistent enum type defined, it's extremely easy to use it. Let's see how!


6.2 Working with Persistent Enumerations
If you were thinking about it, 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 persistent 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.

6.2.1 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.SourceMedia">

<meta attribute="field-description">Media on which track was obtained</meta>

<meta attribute="use-in-tostring">true</meta>

</property>



</class>

...


Because the type of our sourceMedia property names a class that implements the PersistentEnum interface, Hibernate knows to persist it using its built-in enumeration support.

With this addition in place, running ant codegen 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, Date added,

short volume, com.oreilly.hh.SourceMedia sourceMedia,

Set artists, Set 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 Date(),

(short)0, SourceMedia.CD,

new HashSet(), new HashSet());

...

track = new Track("Video Killed the Radio Star",

"vol2/album611/track12.mp3",

Time.valueOf("00:03:49"), new Date(),

(short)0, SourceMedia.VHS,

new HashSet(), new HashSet());


And so on. To match the results shown later, 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.

6.2.2 What just happened?
Our TRACK table now contains an integer 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. Alternately, we can see a more meaningful version of the information by slightly enhancing the query test to print this property for the tracks it retrieves. The necessary changes are in bold in Example 6-4.


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




Example 6-4. 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-5. Tracks with non-null source media values now have "from" and the appropriate media description displayed at the end.

Example 6-5. 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 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 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'd not have needed to make any changes to QueryTest to see this new information. 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 candidate would be to fix AlbumTest.java so it will compile and run after our changes to Track. The easiest fix is to simply hardcode the addAlbumTrack() method to assume everything comes from CDs, as in Example 6-5 (the JavaDoc already excuses such shameful rigidity).

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

throws HibernateException

{

Track track = new Track(title, file, length, new Date(), (short)0,

SourceMedia.CD, new HashSet(), new HashSet());

track.getArtists().add(artist);

// session.save(track);

album.getTracks().add(new AlbumTrack(disc, positionOnDisc, track));

}


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.Album@e0f945[id=0,title=Counterfeit e.p.,

tracks=[com.oreilly.hh.AlbumTrack@1370ab[track=com.oreilly.hh.

Track@49f9fa[id=<null>,title=Compulsion,sourceMedia=Compact Disc]], com.

oreilly.hh.AlbumTrack@ba936a[track=com.oreilly.hh.Track@2421db[id=<null>,

title=In a Manner of Speaking,sourceMedia=Compact Disc]], com.oreilly.hh.

AlbumTrack@2ad974[track=com.oreilly.hh.Track@2a7640[id=<null>,title=Smile in

the Crowd,sourceMedia=Compact Disc]], com.oreilly.hh.

AlbumTrack@b9808e[track=com.oreilly.hh.Track@a721e2[id=<null>,

title=Gone,sourceMedia=Compact Disc]], com.oreilly.hh.

AlbumTrack@a1ad7d[track=com.oreilly.hh.Track@851576[id=<null>,title=Never

Turn Your Back on Mother Earth,sourceMedia=Compact Disc]], com.oreilly.hh.

AlbumTrack@442c19[track=com.oreilly.hh.Track@ab2ddb[id=<null>,

title=Motherless Child,sourceMedia=Compact Disc]]]]


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 will be interesting to see how Hibernate evolves to take advantage of the exciting enum keyword support in Java 1.5 once that's been out for a while. The need to implement PersistentEnum will probably disappear, since all real enums will already extend java.lang.Enum and will have interesting ways to obtain specific members. I hope that as Hibernate evolves to support these new first-class enumerations, it will also allow their symbolic enumeration constants to be stored in the database, rather than requiring the use of a cryptic integer column as it does today. In an ideal world, it will even be able to take advantage of the native support for enumerations provided by some databases.

If you're interested in an alternate approach to persisting typesafe enumerations that can achieve some of these goals today, read on to Chapter 7 in which the mysteries of custom type mapping are explored!

No comments:

Post a Comment