The native Hibernate converter API provides low-level control over value conversion and persistence behavior when ordinary mappings cannot satisfy a shared database schema. A monetary value can be converted to a target currency before storage. section 6.3.3
Hibernate exposes several extension interfaces:
- UserType transforms values through JDBC PreparedStatement and ResultSet access, while controlling caching and dirty checking. section 6.3.3
- CompositeUserType exposes component properties for mapping and query expressions such as amount. section 6.3.3
- ParameterizedType and DynamicParameterizedType configure one adapter differently for each mapping, including access to mapped table and column names. 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.
A MonetaryAmountUserType implements CompositeUserType and DynamicParameterizedType, reading a convertTo parameter and defaulting to USD. section 6.3.3
Currency-specific mappings
Map buyNowPrice with the monetary_amount_usd type and initialPrice with monetary_amount_eur. Both use the same MonetaryAmountUserType while storing amount and currency in separate columns. section 6.3.3
