liveBook · Manning

UserType

As explained in Java Persistence with Spring Data and Hibernate epub

UserType is a Hibernate interface for defining custom mappings between a Java value and JDBC column values, including caching and dirty-checking behavior.

UserType is Hibernate’s low-level extension point for mapping a Java value to JDBC column values. It lets an adapter transform values through PreparedStatement when storing and ResultSet when loading, while controlling Hibernate caching and dirty checking. Unlike a JPA converter, it can adapt any entity property. section 6.3.3

A practical use case is persisting MonetaryAmount values in a legacy shared schema. One property can be converted to US dollars and another to Euros before storage, even when both properties use the same Java type. section 6.3.3

The Hibernate type-system extension points separate responsibilities:

  • UserType performs direct JDBC conversion and governs caching and dirty checking. section 6.3.3
  • CompositeUserType exposes component properties such as amount and currency for property-level access in queries. section 6.3.3
  • ParameterizedType and DynamicParameterizedType supply mapping settings, including the target currency; the dynamic form can access mapped column and table names. section 6.3.3

Mapping one adapter to two currencies

A MonetaryAmountUserType can implement CompositeUserType and DynamicParameterizedType, read a convertTo parameter, and default to USD. Hibernate type definitions can reuse it as monetary_amount_usd and monetary_amount_eur. Item.buyNowPrice maps to amount and currency columns with the USD type, while Item.initialPrice uses the EUR type. Persisting 1.1 USD and loading the entity verifies the converted value; the initial price can be asserted as 2.00 EUR. section 6.3.3

Listing 6.19 The MonetaryAmountUserType class
Path: Ch06/mapping-value-types4/src/main/java/com/manning/javapersistence
 /ch06/converter/MonetaryAmountUserType.java
 
public class MonetaryAmountUserType                                         #1
        implements CompositeUserType, DynamicParameterizedType {            #1
 
    private Currency convertTo;                                             #2
 
    public void setParameterValues(Properties parameters) {                 #3
        String convertToParameter = parameters.getProperty("convertTo");    #4
        this.convertTo = Currency.getInstance(                     
                convertToParameter != null ? convertToParameter : "USD"     #5
        );                                                                  #5
    }
 
    public Class returnedClass() {                                          #6
        return MonetaryAmount.class;                                        #6
    }                                                                       #6
 
    public boolean isMutable() {                                            #7
        return false;                                                       #7
    }                                                                       #7
 
    public Object deepCopy(Object value) {                                  #8
        return value;                                                       #8
    }                                                                       #8
 
    public Serializable disassemble(Object value,                           #9
                                SharedSessionContractImplementor session){  #9
        return value.toString();                                            #9
    }                                                                       #9
 
    public Object assemble(Serializable cached,                             #10
                 SharedSessionContractImplementor session, Object owner) {  #10
        return MonetaryAmount.fromString((String) cached);                  #10
    }                                                                       #10
 
    public Object replace(Object original, Object target,                   #11
                 SharedSessionContractImplementor session, Object owner) {  #11
        return original;                                                    #11
    }                                                                       #11
 
    public boolean equals(Object x, Object y) {                             #12
        return x == y || !(x == null || y == null) && x.equals(y);          #12
    }                                                                       #12
 
    public int hashCode(Object x) {                                         #12
        return x.hashCode();                                                #12
    }                                                                       #12
 
    public Object nullSafeGet(ResultSet resultSet,                          #13
                              String[] names,                               #13
                              SharedSessionContractImplementor session,     #13
                              Object owner) throws SQLException {           #13
        BigDecimal amount = resultSet.getBigDecimal(names[0]);              #14
        if (resultSet.wasNull())                                            #14
            return null;                                                    #14
        Currency currency =                                                 #14
                Currency.getInstance(resultSet.getString(names[1]));        #14
        return new MonetaryAmount(amount, currency);                        #15
    }
 
    public void nullSafeSet(PreparedStatement statement,                    #16
           Object value, int index,                                         #16
           SharedSessionContractImplementor session) throws SQLException {  #16
        if (value == null) {                                                #17
            statement.setNull(                                              #17
                    index,                                                  #17
                    StandardBasicTypes.BIG_DECIMAL.sqlType());              #17
            statement.setNull(                                              #17
                    index + 1,                                              #17
                    StandardBasicTypes.CURRENCY.sqlType());                 #17
        } else {
            MonetaryAmount amount = (MonetaryAmount) value;                 #18
            MonetaryAmount dbAmount = convert(amount, convertTo);           #18
            statement.setBigDecimal(index, dbAmount.getValue());            #19
            statement.setString(index + 1, convertTo.getCurrencyCode());    #19
        }
    }
 
    public MonetaryAmount convert(MonetaryAmount amount,                    #20
                                     Currency toCurrency) {                 #20
        return new MonetaryAmount(                                          #21
                amount.getValue().multiply(new BigDecimal(2)),              #21
                toCurrency                                                  #21
        );                                                                  #21
    }
 
    public String[] getPropertyNames() {                                    #22
        return new String[]{"value", "currency"};                           #22
    }                                                                       #22
 
    public Type[] getPropertyTypes() {                                      #23
        return new Type[]{                                                  #23
                StandardBasicTypes.BIG_DECIMAL,                             #23
                StandardBasicTypes.CURRENCY                                 #23
        };                                                                  #23
    }                                                                       #23
 
    public Object getPropertyValue(Object component,                        #24
                                   int property) {                          #24
        MonetaryAmount monetaryAmount = (MonetaryAmount) component;         #24
        if (property == 0)                                                  #24
            return monetaryAmount.getValue();                               #24
        else                                                                #24
            return monetaryAmount.getCurrency();                            #24
    }                                                                       #24
 
    public void setPropertyValue(Object component,                          #25
                                 int property,                              #25
                                 Object value) {                            #25
        throw new UnsupportedOperationException(                            #25
                "MonetaryAmount is immutable"                               #25
        );                                                                  #25
    }                                                                       #25
}
#1 The interfaces we implement are <kbd>CompositeUserType</kbd> and <kbd>DynamicParameterizedType</kbd>.
#2 The target currency.
#3 The <kbd>setParameterValues</kbd> method is inherited from the <kbd>DynamicParameterizedType</kbd> interface.
#4 Use the <kbd>convertTo</kbd> parameter to determine the target currency when saving a value into the database.
#5 If the parameter hasn’t been set, default to US dollars.
#6 The <kbd>returnedClass</kbd> method adapts the given class, in this case, <kbd>MonetaryAmount</kbd>. This method and the ones to follow are inherited from the <kbd>CompositeUserType</kbd> interface.
#7 Hibernate can enable some optimizations if it knows that <kbd>MonetaryAmount</kbd> is immutable.
#8 If Hibernate has to make a copy of the value, it calls this <kbd>deepCopy</kbd> method. For simple immutable classes like <kbd>MonetaryAmount</kbd>, we can return the given instance.
#9 Hibernate calls the <kbd>disassemble</kbd> method when it stores a value in the global shared second-level cache. We need to return a <kbd>Serializable</kbd> representation. For <kbd>MonetaryAmount</kbd>, a <kbd>String</kbd> representation is an easy solution. Or, because <kbd>MonetaryAmount</kbd> is <kbd>Serializable</kbd>, we could return it directly.
#10 Hibernate calls the <kbd>assemble</kbd> method when it reads the serialized representation from the global shared second-level cache. We create a <kbd>MonetaryAmount</kbd> instance from the <kbd>String</kbd> representation. Or, if we stored a serialized <kbd>MonetaryAmount</kbd>, we could return it directly.
#11 The <kbd>replace</kbd> method is called during <kbd>EntityManager#merge()</kbd> operations. We need to return a copy of the original. Or, if the value type is immutable, like <kbd>MonetaryAmount</kbd>, we can return the original.
#12 Hibernate uses value equality to determine whether the value was changed and the database needs to be updated. We rely on the equality and hash code routines we have already written on the <kbd>MonetaryAmount</kbd> class.
#13 The <kbd>nullSafeGet</kbd> method is called to read the <kbd>ResultSet</kbd> when a <kbd>MonetaryAmount</kbd> value has to be retrieved from the database.
#14 Take the <kbd>amount</kbd> and <kbd>currency</kbd> values as given in the query result.
#15 Create a new instance of <kbd>MonetaryAmount</kbd>.
#16 The <kbd>nullSafeSet</kbd> method is called when a <kbd>MonetaryAmount</kbd> value has to be stored in the database.
#17 If <kbd>MonetaryAmount</kbd> was <kbd>null</kbd>, we call <kbd>setNull()</kbd> to prepare the statement.
#18 Otherwise, we convert the value to the target currency.
#19 We then set the <kbd>amount</kbd> and <kbd>currency</kbd> on the provided <kbd>PreparedStatement</kbd>().
#20 We can implement whatever currency conversion routine we need.
#21 For the sake of this example, we double the value so we can easily test whether the conversion was successful. We’d have to replace this code with a real currency converter in a real application. This <kbd>convert</kbd> method is not a method of the Hibernate <kbd>UserType</kbd> API.
#22 The remaining methods inherited from <kbd>CompositeUserType</kbd> provide the details of the <kbd>MonetaryAmount</kbd> properties, so Hibernate can integrate the class with the query engine. The <kbd>getPropertyNames</kbd> method will return a <kbd>String</kbd> array with two elements, <kbd>value</kbd> and <kbd>currency</kbd>—the names of the properties of the <kbd>MonetaryAmount</kbd> class.
#23 The <kbd>getPropertyTypes</kbd> method will return a <kbd>Type</kbd> array with two elements, <kbd>BIG _DECIMAL</kbd> and <kbd>CURRENCY</kbd>—the types of the properties of the <kbd>MonetaryAmount</kbd> class.
#24 The <kbd>getPropertyValue</kbd> method will return either the <kbd>value</kbd> field or the <kbd>currency</kbd> field of the <kbd>MonetaryAmount</kbd> object, depending on the <kbd>property</kbd> index.
#25 The <kbd>setPropertyValue</kbd> method will not allow any field of the <kbd>MonetaryAmount</kbd> object to be set, as this one is immutable.

Inspect the MonetaryAmountUserType implementation, including parameter handling and the returned Java class. section 6.3.3

How Java Persistence with Spring Data and Hibernate epub covers it

How deep it goes
Descends to PreparedStatement and ResultSet conversion plus cache and dirty-checking control, rather than stopping at standard type conversion.
Examples
Demonstrates MonetaryAmount conversion into USD or EUR across paired amount and currency columns.
What you can do after
Build it from scratch as a configurable Hibernate adapter for custom Java-to-SQL mappings.
How it connects
Connects JDBC, JPA converters, CompositeUserType, ParameterizedType, DynamicParameterizedType, and Hibernate’s type system.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Java Persistence with Spring Data and Hibernate epub. The section links point to the parts of the book it draws on.