5.3. Creating custom mapping types

Object-oriented languages like Java make it easy to define new types by writing new classes. This is a fundamental part of the definition of object-orientation. If we were then limited to the predefined built-in Hibernate mapping types when declaring properties of our persistent classes, we would lose much of Java's expressiveness. Furthermore, our domain model implementation would be tightly coupled to the physical data model, because new type conversions would be impossible.

Most ORM solutions that we have seen provide support for user-defined strategies for performing type conversions. These are often called converters. For example, the user can create a new strategy for persisting a property of JDK type Integer to a VARCHAR column. Hibernate provides a similar, much more powerful, feature called custom mapping types.

First you need to understand when it's appropriate to write your own custom mapping type, and which Hibernate extension point is relevant for you. We'll then write some custom mapping types and explore the options.

5.3.1. Considering custom mapping types

As an example, take the mapping of the Address class from previous chapters, as a component:

<component name="homeAddress" class="Address">

    <property name="street" type="string" column="HOME_STREET"/>
    <property name="city" type="string"   column="HOME_CITY"/>
    <property name="zipcode" type="string" column="HOME_ZIPCODE"/>

</component>

This value type mapping is straightforward; all properties of the new user-defined Java type are mapped to individual columns of a built-in SQL datatype. However, you can alternatively map it as a simple property, with a custom mapping type:

<property name="homeAddress"
          type="auction.persistence.CustomAddressType">

    <column name="HOME_STREET"/>
    <column name="HOME_CITY"/>
    <column name="HOME_ZIPCODE"/>
</property>

This is also probably the first time you've seen a single <property> element with several <column> elements nested inside. We're moving the responsibility for translating and converting between an Address value type (it isn't even named anywhere) and the named three columns to a separate class: auction.persistence.CustomAddressType. This class is now responsible for loading and saving this property. Note that no Java code changes in the domain model implementation—the homeAddress property is of type Address.

Granted, the benefit of replacing a component mapping with a custom mapping type is dubious in this case. As long as you require no special conversion when loading and saving this object, the CustomAddressType you now have to write is just additional work. However, you can already see that custom mapping types provide an additional buffer—something that may come in handy in the long run when extra conversion is required. Of course, there are better use cases for custom mapping types, as you'll soon see. (Many examples of useful Hibernate mapping types can be found on the Hibernate community website.)

Let's look at the Hibernate extension points for the creation of custom mapping types.

5.3.2. The extension points

Hibernate provides several interfaces that applications may use when defining custom mapping types. These interfaces reduce the work involved in creating new mapping types and insulate the custom type from changes to the Hibernate core. This allows you to easily upgrade Hibernate and keep your existing custom mapping types.

The extension points are as follows:

  • org.hibernate.usertype.UserType—The basic extension point, which is useful in many situations. It provides the basic methods for custom loading and storing of value type instances.

  • org.hibernate.usertype.CompositeUserType—An interface with more methods than the basic UserType, used to expose internals about your value type class to Hibernate, such as the individual properties. You can then refer to these properties in Hibernate queries.

  • org.hibernate.usertype.UserCollectionType—A rarely needed interface that's used to implement custom collections. A custom mapping type implementing this interface isn't declared on a property mapping but is useful only for custom collection mappings. You have to implement this type if you want to persist a non-JDK collection and preserve additional semantics persistently. We discuss collection mappings and this extension point in the next chapter.

  • org.hibernate.usertype.EnhancedUserType—An interface that extends UserType and provides additional methods for marshalling value types to and from XML representations, or enables a custom mapping type for use in identifier and discriminator mappings.

  • org.hibernate.usertype.UserVersionType—An interface that extends UserType and provides additional methods enabling the custom mapping type for usage in entity version mappings.

  • org.hibernate.usertype.ParameterizedType—A useful interface that can be combined with all others to provide configuration settings—that is, parameters defined in metadata. For example, you can write a single MoneyConverter that knows how to translate values into Euro or US dollars, depending on a parameter in the mapping.

We'll now create some custom mapping types. You shouldn't consider this an unnecessary exercise, even if you're happy with the built-in Hibernate mapping types. In our experience, every sophisticated application has many good use cases for custom mapping types.

5.3.3. The case for custom mapping types

The Bid class defines an amount property, and the Item class defines an initialPrice property; both are monetary values. So far, we've used only a simple BigDecimal to represent the value, mapped with big_decimal to a single NUMERIC column.

Suppose you want to support multiple currencies in the auction application and that you have to refactor the existing domain model for this (customer-driven) change. One way to implement this change would be to add new properties to Bid and Item: amountCurrency and initialPriceCurrency. You could then map these new properties to additional VARCHAR columns with the built-in currency mapping type. We hope you never use this approach!

Instead, you should create a new MonetaryAmount class that encapsulates both currency and amount. Note that this is a class of your domain model; it doesn't have any dependency on Hibernate interfaces:

public class MonetaryAmount implements Serializable {

    private final BigDecimal amount;
    private final Currency currency;

    public MonetaryAmount(BigDecimal amount, Currency currency) {
        this.amount = amount;
        this.currency = currency;
    }

    public BigDecimal getAmount() { return amount; }

    public Currency getCurrency() { return currency; }

    public boolean equals(Object o) { ... }
    public int hashCode() { ...}
}

We have made MonetaryAmount an immutable class. This is a good practice in Java because it simplifies coding. Note that you have to implement equals() and hashCode() to finish the class (there is nothing special to consider here). You use this new MonetaryAmount to replace the BigDecimal of the initialPrice property in Item. You can and should use it for all other BigDecimal prices in any persistent classes, such as the Bid.amount, and in business logic—for example, in the billing system.

Let's map the refactored initialPrice property of Item, with its new MonetaryAmount type to the database.

5.3.4. Creating a UserType

Imagine that you're working with a legacy database that represents all monetary amounts in USD. The application is no longer restricted to a single currency (that was the point of the refactoring), but it takes some time for the database team to make the changes. You need to convert the amount to USD when persisting MonetaryAmount objects. When you load from the database, you convert it back to the currency the user selected in his or her preferences.

Create a new MonetaryAmountUserType class that implements the Hibernate interface UserType. This is your custom mapping type, shown in listing 5.4.

Listing 5-4. Custom mapping type for monetary amounts in USD
 public class MonetaryAmountUserType
        implements UserType {

    public int[] sqlTypes() {
return new int[]{ Hibernate.BIG_DECIMAL.sqlType() }; } public Class returnedClass() { return MonetaryAmount.class; }
public boolean isMutable() { return false; }
public Object deepCopy(Object value) { return value; }
public Serializable disassemble(Object value)
{ return (Serializable) value; } public Object assemble(Serializable cached, Object owner)
{ return cached; } public Object replace(Object original,
Object target, Object owner) { return original; } public boolean equals(Object x, Object y) {
if (x == y) return true; if (x == null || y == null) return false; return x.equals(y); } public int hashCode(Object x) { return x.hashCode(); } public Object nullSafeGet(ResultSet resultSet,
String[] names, Object owner) throws SQLException { BigDecimal valueInUSD = resultSet.getBigDecimal(names[0]); // Deferred check after first read if (resultSet.wasNull()) return null; Currency userCurrency = User.getPreferences().getCurrency(); MonetaryAmount amount = new MonetaryAmount(valueInUSD, "USD"); return amount.convertTo(userCurrency); } public void nullSafeSet(PreparedStatement statement,
Object value, int index) throws HibernateException, SQLException { if (value == null) { statement.setNull(index, Hibernate.BIG_DECIMAL.sqlType()); } else { MonetaryAmount anyCurrency = (MonetaryAmount)value; MonetaryAmount amountInUSD = MonetaryAmount.convert( anyCurrency, Currency.getInstance("USD") ); statement.setBigDecimal(index, amountInUSD.getAmount ()); } } }

❶ The sqlTypes() method tells Hibernate what SQL column types to use for DDL schema generation. Notice that this method returns an array of type codes. A UserType may map a single property to multiple columns, but this legacy data model has only a single numeric column. By using the Hibernate.BIG_DECIMAL.sqlType() method, you let Hibernate decide the exact SQL datatype for the given database dialect. Alternatively, return a constant from java.sql.Types.

❷ The returnedClass() method tells Hibernate what Java value type class is mapped by this UserType.

❸ Hibernate can make some minor performance optimizations for immutable types like this one, for example, when comparing snapshots during dirty checking. The isMutable() method tells Hibernate that this type is immutable.

❹ The UserType is also partially responsible for creating a snapshot of a value in the first place. Because MonetaryAmount is an immutable class, the deepCopy() method returns its argument. In the case of a mutable type, it would need to return a copy of the argument to be used as the snapshot value.

❺ The disassemble() method is called when Hibernate puts a MonetaryAmount into the second-level cache. As you'll learn later, this is a cache of data that stores information in a serialized form.

❻ The assemble() method does the opposite of disassembly: It can transform cached data into an instance of MonetaryAmount. As you can see, implementation of both routines is easy for immutable types.

❼ Implement replace() to handle merging of detached object state. As you'll see later in the book, the process of merging involves an original and a target object, whose state must be combined. Again, for immutable value types, return the first argument. For mutable types, at least return a deep copy of the first argument. For mutable types that have component fields, you probably want to apply a recursive merging routine.

❽ The UserType is responsible for dirty checking property values. The equals() method compares the current property value to a previous snapshot and determines whether the property is dirty and must by saved to the database. The hashCode() of two equal value typed instances has to be the same. We usually delegate this method to the actual value type class—in this case, the hashCode() method of the given MonetaryAmount object.

❾ The nullSafeGet() method retrieves the property value from the JDBC ResultSet. You can also access the owner of the component if you need it for the conversion. All database values are in USD, so you convert it to the currency the user has currently set in his preferences. (Note that it's up to you to implement this conversion and preference handling.)

❿ The nullSafeSet() method writes the property value to the JDBC PreparedStatement. This method takes whatever currency is set and converts it to a simple BigDecimal USD amount before saving.

You now map the initialPrice property of Item as follows:

<property name="initialPrice"
          column="INITIAL_PRICE"
          type="persistence.MonetaryAmountUserType"/>

Note that you place the custom user type into the persistence package; it's part of the persistence layer of the application, not the domain model or business layer.

To use a custom type in annotations, you have to add a Hibernate extension:

@org.hibernate.annotations.Type(
    type = " persistence.MonetaryAmountUserType"
)
@Column(name = "INITIAL_PRICE")
private MonetaryAmount initialPrice;

This is the simplest kind of transformation that a UserType can perform. Much more sophisticated things are possible. A custom mapping type can perform validation; it can read and write data to and from an LDAP directory; it can even retrieve persistent objects from a different database. You're limited mainly by your imagination.

In reality, we'd prefer to represent both the amount and currency of monetary amounts in the database, especially if the schema isn't legacy but can be defined (or updated quickly). Let's assume you now have two columns available and can store the MonetaryAmount without much conversion. A first option may again be a simple <component> mapping. However, let's try to solve it with a custom mapping type.

(Instead of writing a new custom type, try to adapt the previous example for two columns. You can do this without changing the Java domain model classes—only the converter needs to be updated for this new requirement and the additional column named in the mapping.)

The disadvantage of a simple UserType implementation is that Hibernate doesn't know anything about the individual properties inside a MonetaryAmount. All it knows is the custom type class and the column names. The Hibernate query engine (discussed in more detail later) doesn't know how to query for amount or a particular currency.

You write a CompositeUserType if you need the full power of Hibernate queries. This (slightly more complex) interface exposes the properties of the MonetaryAmount to Hibernate queries. We'll now map it again with this more flexible customization interface to two columns, effectively producing an equivalent to a component mapping.

5.3.5. Creating a CompositeUserType

To demonstrate the flexibility of custom mappings types, you don't change the MonetaryAmount class (and other persistent classes) at all—you change only the custom mapping type, as shown in listing 5.5.

Listing 5-5. Custom mapping type for monetary amounts in new database schemas
public class MonetaryAmountCompositeUserType
        implements CompositeUserType {

    // public int[] sqlTypes()... 
public Class returnedClass... public boolean isMutable... public Object deepCopy... public Serializable disassemble... public Object assemble... public Object replace... public boolean equals... public int hashCode... public Object nullSafeGet(ResultSet resultSet,
String[] names, SessionImplementor session, Object owner) throws SQLException { BigDecimal value = resultSet.getBigDecimal( names[0] ); if (resultSet.wasNull()) return null; Currency currency = Currency.getInstance(resultSet.getString( names[1] ) ); return new MonetaryAmount(value, currency); } public void nullSafeSet(PreparedStatement statement,
Object value, int index, SessionImplementor session) throws SQLException { if (value==null) { statement.setNull(index, Hibernate.BIG_DECIMAL.sqlType()); statement.setNull(index+1, Hibernate.CURRENCY.sqlType()); } else { MonetaryAmount amount = (MonetaryAmount) value; String currencyCode = amount.getCurrency().getCurrencyCode(); statement.setBigDecimal( index, amount.getAmount() ); statement.setString( index+1, currencyCode ); } } public String[] getPropertyNames() {
return new String[] { "amount", "currency" }; } public Type[] getPropertyTypes() {
return new Type[] { Hibernate.BIG_DECIMAL, Hibernate.CURRENCY }; } public Object getPropertyValue(Object component, int property) {
MonetaryAmount monetaryAmount = (MonetaryAmount) component; if (property == 0) return monetaryAmount.getAmount(); else return monetaryAmount.getCurrency(); } public void setPropertyValue(Object component,
int property, Object value) { throw new UnsupportedOperationException("Immutable MonetaryAmount!"); } }

❶ The CompositeUserType interface requires the same housekeeping methods as the UserType you created earlier. However, the sqlTypes() method is no longer needed.

❷ Loading a value now is straightforward: You transform two column values in the result set to two property values in a new MonetaryAmount instance.

❸ Saving a value involves setting two parameters on the prepared statement.

❹ A CompositeUserType exposes the properties of the value type through getPropertyNames().

❺ The properties each have their own type, as defined by getPropertyTypes(). The types of the SQL columns are now implicit from this method.

❻ The getPropertyValue() method returns the value of an individual property of the MonetaryAmount.

❼ The setPropertyValue() method sets the value of an individual property of the MonetaryAmount.

The initialPrice property now maps to two columns, so you need to declare both in the mapping file. The first column stores the value; the second stores the currency of the MonetaryAmount:

<property name="initialPrice"
          type="persistence.MonetaryAmountCompositeUserType">
    <column name="INITIAL_PRICE"/>
    <column name="INITIAL_PRICE_CURRENCY"/>
</property>

If Item is mapped with annotations, you have to declare several columns for this property. You can't use the javax.persistence.Column annotation several times, so a new, Hibernate-specific annotation is needed:

@org.hibernate.annotations.Type(
    type = "persistence.MonetaryAmountUserType"
)
@org.hibernate.annotations.Columns(columns = {
    @Column(name="INITIAL_PRICE"),
    @Column(name="INITIAL_PRICE_CURRENCY", length = 2)
})
private MonetaryAmount initialPrice;

In a Hibernate query, you can now refer to the amount and currency properties of the custom type, even though they don't appear anywhere in the mapping document as individual properties:

from Item i
where i.initialPrice.amount > 100.0
  and i.initialPrice.currency = 'AUD'

You have extended the buffer between the Java object model and the SQL database schema with the new custom composite type. Both representations are now more robust to changes. Note that the number of columns isn't relevant for your choice of UserType versus CompositeUserType—only your desire to expose value type properties for Hibernate queries.

Parameterization is a helpful feature for all custom mapping types.

5.3.6. Parameterizing custom types

Let's assume that you face the initial problem again: conversion of money to a different currency when storing it to the database. Often, problems are more subtle than a generic conversion; for example, you may store US dollars in some tables and Euros in others. You still want to write a single custom mapping type for this, which can do arbitrary conversions. This is possible if you add the ParameterizedType interface to your UserType or CompositeUserType classes:

public class MonetaryAmountConversionType
    implements UserType, ParameterizedType {

    // Configuration parameter
    private Currency convertTo;

    public void setParameterValues(Properties parameters) {
       this.convertTo = Currency.getInstance(
                            parameters.getProperty("convertTo")
                        );
    }

    // ... Housekeeping methods

    public Object nullSafeGet(ResultSet resultSet,
                              String[] names,
                              SessionImplementor session,
                              Object owner)
           throws SQLException {

       BigDecimal value = resultSet.getBigDecimal( names[0] );
       if (resultSet.wasNull()) return null;
       // When loading, take the currency from the database
       Currency currency = Currency.getInstance(
                           resultSet.getString( names[1] )
                           );
       return new MonetaryAmount(value, currency);
    }

    public void nullSafeSet(PreparedStatement statement,
                            Object value,
                            int index,
                            SessionImplementor session)
           throws SQLException {

       if (value==null) {
           statement.setNull(index, Types.NUMERIC);
       } else {
           MonetaryAmount amount = (MonetaryAmount) value;
           // When storing, convert the amount to the
           // currency this converter was parameterized with
           MonetaryAmount dbAmount =
                MonetaryAmount.convert(amount, convertTo);
           statement.setBigDecimal( index, dbAmount.getAmount() );
           statement.setString( index+1,
                                dbAmount.getCurrencyCode() );
       }
    }
}

We left out the usual mandatory housekeeping methods in this example. The important additional method is setParameterValues() of the ParameterizedType interface. Hibernate calls this method on startup to initialize this class with a convertTo parameter. The nullSafeSet() methods uses this setting to convert to the target currency when saving a MonetaryAmount. The nullSafeGet() method takes the currency that is present in the database and leaves it to the client to deal with the currency of a loaded MonetaryAmount (this asymmetric implementation isn't the best idea, naturally).

You now have to set the configuration parameters in your mapping file when you apply the custom mapping type. A simple solution is the nested <type> mapping on a property:

<property name="initialPrice">
    <column name="INITIAL_PRICE"/>
    <column name="INITIAL_PRICE_CUR"/>
    <type name="persistence.MonetaryAmountConversionType">
        <param name="convertTo">USD</param>
    </type>
</property>

However, this is inconvenient and requires duplication if you have many monetary amounts in your domain model. A better strategy uses a separate definition of the type, including all parameters, under a unique name that you can then reuse across all your mappings. You do this with a separate <typedef>, an element (you can also use it without parameters):

<typedef class="persistence.MonetaryAmountConversionType"
          name="monetary_amount_usd">
    <param name="convertTo">USD</param>
</typedef>

<typedef class="persistence.MonetaryAmountConversionType"
          name="monetary_amount_eur">
    <param name="convertTo">EUR</param>
</typedef>

What we show here is a binding of a custom mapping type with some arguments to the names monetary_amount_usd and monetary_amount_eur. This definition can be placed anywhere in your mapping files; it's a child element of <hibernate-mapping> (as mentioned earlier in the book, larger applications have often one or several MyCustomTypes.hbm.xml files with no class mappings). With Hibernate extensions, you can define named custom types with parameters in annotations:

@org.hibernate.annotations.TypeDefs({
    @org.hibernate.annotations.TypeDef(
        name="monetary_amount_usd",
        typeClass = persistence.MonetaryAmountConversionType.class,
        parameters = { @Parameter(name="convertTo", value="USD") }
    ),
    @org.hibernate.annotations.TypeDef(
        name="monetary_amount_eur",
        typeClass = persistence.MonetaryAmountConversionType.class,
        parameters = { @Parameter(name="convertTo", value="EUR") }
    )
})

This annotation metadata is global as well, so it can be placed outside any Java class declaration (right after the import statements) or in a separate file, package-info.java, as discussed in chapter 2, section 2.2.1, "Using Hibernate Annotations." A good location in this system is in a package-info.java file in the persistence package.

In XML mapping files and annotation mappings, you now refer to the defined type name instead of the fully qualified class name of your custom type:

<property name="initialPrice"
          type="monetary_amount_usd">
    <column name="INITIAL_PRICE"/>
    <column name="INITIAL_PRICE_CUR"/>
</property>

@org.hibernate.annotations.Type(type = "monetary_amount_eur")
@org.hibernate.annotations.Columns({
  @Column(name = "BID_AMOUNT"),
  @Column(name = "BID_AMOUNT_CUR")
})
private MonetaryAmount bidAmount;

Let's look at a different, extremely important, application of custom mapping types. The type-safe enumeration design pattern can be found in almost all applications.

5.3.7. Mapping enumerations

An enumeration type is a common Java idiom where a class has a constant (small) number of immutable instances. In CaveatEmptor, this can be applied to credit cards: for example, to express the possible types a user can enter and the application offers (Mastercard, Visa, and so on). Or, you can enumerate the possible ratings a user can submit in a Comment, about a particular auction.

In older JDKs, you had to implement such classes (let's call them CreditCardType and Rating) yourself, following the type-safe enumeration pattern. This is still the right way to do it if you don't have JDK 5.0; the pattern and compatible custom mapping types can be found on the Hibernate community website.

Using enumerations in JDK 5.0

If you use JDK 5.0, you can use the built-in language support for type-safe enumerations. For example, a Rating class looks as follows:

package auction.model;

public enum Rating {
    EXCELLENT, OK, BAD
}

The Comment class has a property of this type:

public class Comment {
    ...
    private Rating rating;
    private Item auction;
    ...
}

This is how you use the enumeration in the application code:

Comment goodComment =
    new Comment(Rating.EXCELLENT, thisAuction);

You now have to persist this Comment instance and its Rating. One approach is to use the actual name of the enumeration and save it to a VARCHAR column in the COMMENTS table. This RATING column will then contain EXCELLENT, OK, or BAD, depending on the Rating given.

Let's write a Hibernate UserType that can load and store VARCHAR-backed enumerations, such as the Rating.

Writing a custom enumeration handler

Instead of the most basic UserType interface, we now want to show you the EnhancedUserType interface. This interface allows you to work with the Comment entity in XML representation mode, not only as a POJO (see the discussion of data representations in chapter 3, section 3.4, "Alternative entity representation"). Furthermore, the implementation you'll write can support any VARCHAR-backed enumeration, not only Rating, thanks to the additional ParameterizedType interface.

Look at the code in listing 5.6.

Listing 5-6. Custom mapping type for string-backed enumerations
public class StringEnumUserType
        implements EnhancedUserType, ParameterizedType {

    private Class<Enum> enumClass;
    public void setParameterValues(Properties parameters) { 
String enumClassName = parameters.getProperty("enumClassname"); try { enumClass = ReflectHelper.classForName(enumClassName); } catch (ClassNotFoundException cnfe) { throw new HibernateException("Enum class not found", cnfe); } } public Class returnedClass() {
return enumClass; } public int[] sqlTypes() {
return new int[] { Hibernate.STRING.sqlType() }; } public boolean isMutable...
public Object deepCopy... public Serializable disassemble... public Object replace... public Object assemble... public boolean equals... public int hashCode... public Object fromXMLString(String xmlValue) {
return Enum.valueOf(enumClass, xmlValue); } public String objectToSQLString(Object value) { return ''' + ( (Enum) value ).name() + '''; } public String toXMLString(Object value) { return ( (Enum) value ).name(); } public Object nullSafeGet(ResultSet rs,
String[] names, Object owner) throws SQLException { String name = rs.getString( names[0] ); return rs.wasNull() ? null : Enum.valueOf(enumClass, name);
} public void nullSafeSet(PreparedStatement st, Object value, int index) throws SQLException { if (value == null) { st.setNull(index, Hibernate.STRING.sqlType()); } else { st.setString( index, ( (Enum) value ).name() ); } } }

❶ The configuration parameter for this custom mapping type is the name of the enumeration class it's used for, such as Rating.

❷ It's also the class that is returned from this method.

❸ A single VARCHAR column is needed in the database table. You keep it portable by letting Hibernate decide the SQL datatype.

❹ These are the usual housekeeping methods for an immutable type.

❺ The following three methods are part of the EnhancedUserType and are used for XML marshalling.

❻ When you're loading an enumeration, you get its name from the database and create an instance.

❼ When you're saving an enumeration, you store its name.

Next, you'll map the rating property with this new custom type.

Mapping enumerations with XML and annotations

In the XML mapping, first create a custom type definition:

<typedef class="persistence.StringEnumUserType"
         name="rating">
    <param name="enumClassname">auction.model.Rating</param>
</typedef>

You can now use the type named rating in the Comment class mapping:

<property  name="rating"
           column="RATING"
           type="rating"
           not-null="true"
           update="false"
           access="field"/>

Because ratings are immutable, you map it as update="false" and enable direct field access (no setter method for immutable properties). If other classes besides Comment have a Rating property, use the defined custom mapping type again.

The definition and declaration of this custom mapping type in annotations looks the same as the one you did in the previous section.

On the other hand, you can rely on the Java Persistence provider to persist enumerations. If you have a property in one of your annotated entity classes of type java.lang.Enum (such as the rating in your Comment), and it isn't marked as @Transient or transient (the Java keyword), the Hibernate JPA implementation must persist this property out of the box without complaining; it has a built-in type that handles this. This built-in mapping type has to default to a representation of an enumeration in the database. The two common choices are string representation, as you implemented for native Hibernate with a custom type, or ordinal representation. An ordinal representation saves the position of the selected enumeration option: for example, 1 for EXCELLENT, 2 for OK, and 3 for BAD. The database column also defaults to a numeric column. You can change this default enumeration mapping with the Enumerated annotation on your property:

public class Comment {
    ...

    @Enumerated(EnumType.STRING)
    @Column(name = "RATING", nullable = false, updatable = false)
    private Rating rating;
    ...
}

You've now switched to a string-based representation, effectively the same representation your custom type can read and write. You can also use a JPA XML descriptor:

<entity class="auction.model.Item" access="PROPERTY">
  <attributes>
    ...
    <basic name="rating">
        <column name="RATING" nullable="false" updatable="false"/>
        <enumerated>STRING</enumerated>
    </basic>
  </attributes>
</entity>

You may (rightfully) ask why you have to write your own custom mapping type for enumerations when obviously Hibernate, as a Java Persistence provider, can persist and load enumerations out of the box. The secret is that Hibernate Annotations includes several custom mapping types that implement the behavior defined by Java Persistence. You could use these custom types in XML mappings; however, they aren't user friendly (they need many parameters) and weren't written for that purpose. You can check the source (such as org.hibernate.type.EnumType in Hibernate Annotations) to learn their parameters and decide if you want to use them directly in XML.

Querying with custom mapping types

One further problem you may run into is using enumerated types in Hibernate queries. For example, consider the following query in HQL that retrieves all comments that are rated "bad":

Query q =
    session.createQuery(
      "from Comment c where c.rating = auction.model.Rating.BAD"
    );

Although this query works if you persist your enumeration as a string (the query parser uses the enumeration value as a constant), it doesn't work if you selected ordinal representation. You have to use a bind parameter and set the rating value for the comparison programmatically:

Query q =
    session.createQuery("from Comment c where c.rating = :rating");
Properties params = new Properties();
params.put("enumClassname",
           "auction.model.Rating");
q.setParameter("rating", Rating.BAD,
               Hibernate.custom(StringEnumUserType.class, params)
              );

The last line in this example uses the static helper method Hibernate.custom() to convert the custom mapping type to a Hibernate Type; this is a simple way to tell Hibernate about your enumeration mapping and how to deal with the Rating.BAD value. Note that you also have to tell Hibernate about any initialization properties the parameterized type may need.

Unfortunately, there is no API in Java Persistence for arbitrary and custom query parameters, so you have to fall back to the Hibernate Session API and create a Hibernate Query object.

We recommend that you become intimately familiar with the Hibernate type system and that you consider the creation of custom mapping types an essential skill—it will be useful in every application you develop with Hibernate or JPA.

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

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