JavaPersistenceWithHibernate第二版笔记-第五章-Mapping value types-007UserTypes的用法(@org.hibernate.annotations.Type、@org.hibernate.annotations.TypeDefs、CompositeUserType、DynamicParameterizedType、、、)
一、结构
二、Hibernate支持的UserTypes接口
UserType —You can transform values by interacting with the plain JDBC PreparedStatement (when storing data) and ResultSet (when loading data).By implementing this interface, you can also control how Hibernate caches and
dirty-checks values. The adapter for MonetaryAmount has to implement this interface.
CompositeUserType —This extends UserType , providing Hibernate with more details about your adapted class. You can tell Hibernate that the MonetaryAmount component has two properties: amount and currency . You can then reference these properties in queries with dot notation: for example, select avg(i.buyNowPrice.amount) from Item i .
ParameterizedUserType —This provides settings to your adapter in mappings.You have to implement this interface for the MonetaryAmount conversion,because in some mappings you want to convert the amount to US dollars and in
other mappings to Euros. You only have to write a single adapter and can customize its behavior when mapping a property.
DynamicParameterizedType —This more powerful settings API gives you access to dynamic information in the adapter, such as the mapped column and table names. You might as well use this instead of ParameterizedUserType ; there is no additional cost or complexity.
EnhancedUserType —This is an optional interface for adapters of identifier properties and discriminators. Unlike JPA converters, a UserType in Hibernate can be an adapter for any kind of entity property. Because MonetaryAmount won’t be the type of an identifier property or discriminator, you won’t need it.
UserVersionType —This is an optional interface for adapters of version properties.
UserCollectionType —This rarely needed interface is used to implement custom collections. You have to implement it to persist a non- JDK collection and preserve additional semantics.
三、代码
1.
1 package org.jpwh.converter; 2 3 import org.hibernate.engine.spi.SessionImplementor; 4 import org.hibernate.type.StandardBasicTypes; 5 import org.hibernate.type.Type; 6 import org.hibernate.usertype.CompositeUserType; 7 import org.hibernate.usertype.DynamicParameterizedType; 8 import org.jpwh.model.advanced.MonetaryAmount; 9 10 import java.io.Serializable; 11 import java.lang.annotation.Annotation; 12 import java.math.BigDecimal; 13 import java.sql.PreparedStatement; 14 import java.sql.ResultSet; 15 import java.sql.SQLException; 16 import java.util.Currency; 17 import java.util.Properties; 18 19 public class MonetaryAmountUserType 20 implements CompositeUserType, DynamicParameterizedType { 21 22 protected Currency convertTo; 23 24 public void setParameterValues(Properties parameters) { 25 26 /** 27 * You can access some dynamic parameters here, such as the 28 * name of the mapped columns, the mapped (entity) table, or even the 29 * annotations on the field/getter of the mapped property. We won't need 30 * them in this example though. 31 */ 32 ParameterType parameterType = 33 (ParameterType) parameters.get(PARAMETER_TYPE); 34 String[] columns = parameterType.getColumns(); 35 String table = parameterType.getTable(); 36 Annotation[] annotations = parameterType.getAnnotationsMethod(); 37 38 /** 39 * We only use the <code>convertTo</code> parameter to 40 * determine the target currency when saving a value into the database. 41 * If the parameter hasn't been set, we default to US Dollar. 42 */ 43 String convertToParameter = parameters.getProperty("convertTo"); 44 this.convertTo = Currency.getInstance( 45 convertToParameter != null ? convertToParameter : "USD" 46 ); 47 } 48 49 /** 50 * The method <code>returnedClass</code> adapts the given class, in this case 51 * <code>MonetaryAmount</code>. 52 */ 53 public Class returnedClass() { 54 return MonetaryAmount.class; 55 } 56 57 /** 58 * Hibernate can enable some optimizations if it knows 59 * that <code>MonetaryAmount</code> is immutable. 60 */ 61 public boolean isMutable() { 62 return false; 63 } 64 65 /** 66 * If Hibernate has to make a copy of the value, it will call 67 * this method. For simple immutable classes like <code>MonetaryAmount</code>, 68 * you can return the given instance. 69 */ 70 public Object deepCopy(Object value) { 71 return value; 72 } 73 74 /** 75 * Hibernate calls <code>disassemble</code> when it stores a value in the global shared second-level 76 * cache. You need to return a <code>Serializable</code> representation. For <code>MonetaryAmount</code>, 77 * a <code>String</code> representation is an easy solution. Or, because <code>MonetaryAmount</code> is actually 78 * <code>Serializable</code>, you could return it directly. 79 */ 80 public Serializable disassemble(Object value, 81 SessionImplementor session) { 82 return value.toString(); 83 } 84 85 /** 86 * Hibernate calls this method when it reads the serialized 87 * representation from the global shared second-level cache. We create a 88 * <code>MonetaryAmount</code> instance from the <code>String</code> 89 * representation. Or, if have stored a serialized <code>MonetaryAmount</code>, 90 * you could return it directly. 91 */ 92 public Object assemble(Serializable cached, 93 SessionImplementor session, Object owner) { 94 return MonetaryAmount.fromString((String) cached); 95 } 96 97 /** 98 * Called during <code>EntityManager#merge()</code> operations, you 99 * need to return a copy of the <code>original</code>. Or, if your value type is 100 * immutable, like <code>MonetaryAmount</code>, you can simply return the original. 101 */ 102 public Object replace(Object original, Object target, 103 SessionImplementor session, Object owner) { 104 return original; 105 } 106 107 /** 108 * Hibernate will use value equality to determine whether the value 109 * was changed, and the database needs to be updated. We rely on the equality 110 * routine we have already written on the <code>MonetaryAmount</code> class. 111 */ 112 public boolean equals(Object x, Object y) { 113 return x == y || !(x == null || y == null) && x.equals(y); 114 } 115 116 public int hashCode(Object x) { 117 return x.hashCode(); 118 } 119 120 /** 121 * Called to read the <code>ResultSet</code>, when a 122 * <code>MonetaryAmount</code> value has to be retrieved from the database. 123 * We take the <code>amount</code> and <code>currency</code> values as given 124 * in the query result, and create a new instance of <code>MonetaryAmount</code>. 125 */ 126 public Object nullSafeGet(ResultSet resultSet, 127 String[] names, 128 SessionImplementor session, 129 Object owner) throws SQLException { 130 131 BigDecimal amount = resultSet.getBigDecimal(names[0]); 132 if (resultSet.wasNull()) 133 return null; 134 Currency currency = 135 Currency.getInstance(resultSet.getString(names[1])); 136 return new MonetaryAmount(amount, currency); 137 } 138 139 /** 140 * Called when a <code>MonetaryAmount</code> value has 141 * to be stored in the database. We convert the value to the target currency, 142 * then set the <code>amount</code> and <code>currency</code> on the 143 * provided <code>PreparedStatement</code>. (Unless the <code>MonetaryAmount</code> 144 * was <code>null</code>, in that case, we call <code>setNull()</code> to 145 * prepare the statement.) 146 */ 147 public void nullSafeSet(PreparedStatement statement, 148 Object value, 149 int index, 150 SessionImplementor session) throws SQLException { 151 152 if (value == null) { 153 statement.setNull( 154 index, 155 StandardBasicTypes.BIG_DECIMAL.sqlType()); 156 statement.setNull( 157 index + 1, 158 StandardBasicTypes.CURRENCY.sqlType()); 159 } else { 160 MonetaryAmount amount = (MonetaryAmount) value; 161 // When saving, convert to target currency 162 MonetaryAmount dbAmount = convert(amount, convertTo); 163 statement.setBigDecimal(index, dbAmount.getValue()); 164 statement.setString(index + 1, convertTo.getCurrencyCode()); 165 } 166 } 167 168 /** 169 * Here you can implement whatever currency conversion routine 170 * you need. For the sake of the example, we simply double the value so we 171 * can easily test if conversion was successful. You'll have to replace this 172 * code with a real currency converter in a real application. It's not a 173 * method of the Hibernate <code>UserType</code> API. 174 */ 175 protected MonetaryAmount convert(MonetaryAmount amount, 176 Currency toCurrency) { 177 return new MonetaryAmount( 178 amount.getValue().multiply(new BigDecimal(2)), 179 toCurrency 180 ); 181 } 182 183 public String[] getPropertyNames() { 184 return new String[]{"value", "currency"}; 185 } 186 187 public Type[] getPropertyTypes() { 188 return new Type[]{ 189 StandardBasicTypes.BIG_DECIMAL, 190 StandardBasicTypes.CURRENCY 191 }; 192 } 193 194 public Object getPropertyValue(Object component, 195 int property) { 196 MonetaryAmount monetaryAmount = (MonetaryAmount) component; 197 if (property == 0) 198 return monetaryAmount.getValue(); 199 else 200 return monetaryAmount.getCurrency(); 201 } 202 203 public void setPropertyValue(Object component, 204 int property, 205 Object value) { 206 throw new UnsupportedOperationException( 207 "MonetaryAmount is immutable" 208 ); 209 } 210 211 // ... 212 }
2.
1 @org.hibernate.annotations.TypeDefs({ 2 @org.hibernate.annotations.TypeDef( 3 name = "monetary_amount_usd", 4 typeClass = MonetaryAmountUserType.class, 5 parameters = {@Parameter(name = "convertTo", value = "USD")} 6 ), 7 @org.hibernate.annotations.TypeDef( 8 name = "monetary_amount_eur", 9 typeClass = MonetaryAmountUserType.class, 10 parameters = {@Parameter(name = "convertTo", value = "EUR")} 11 ) 12 }) 13 package org.jpwh.converter; 14 15 import org.hibernate.annotations.Parameter;
3.
1 package org.jpwh.model.advanced.usertype; 2 3 import org.jpwh.model.advanced.MonetaryAmount; 4 5 import javax.persistence.Column; 6 import javax.persistence.Entity; 7 import javax.persistence.GeneratedValue; 8 import javax.persistence.Id; 9 import javax.validation.constraints.NotNull; 10 11 @Entity 12 public class Item { 13 14 @Id 15 @GeneratedValue(generator = "ID_GENERATOR") 16 protected Long id; 17 18 @NotNull 19 protected String name; 20 21 @NotNull 22 @org.hibernate.annotations.Type( 23 type = "monetary_amount_usd" 24 ) 25 @org.hibernate.annotations.Columns(columns = { 26 @Column(name = "BUYNOWPRICE_AMOUNT"), 27 @Column(name = "BUYNOWPRICE_CURRENCY", length = 3) 28 }) 29 protected MonetaryAmount buyNowPrice; 30 31 @NotNull 32 @org.hibernate.annotations.Type( 33 type = "monetary_amount_eur" 34 ) 35 @org.hibernate.annotations.Columns(columns = { 36 @Column(name = "INITIALPRICE_AMOUNT"), 37 @Column(name = "INITIALPRICE_CURRENCY", length = 3) 38 }) 39 protected MonetaryAmount initialPrice; 40 41 public Long getId() { 42 return id; 43 } 44 45 public String getName() { 46 return name; 47 } 48 49 public void setName(String name) { 50 this.name = name; 51 } 52 53 public MonetaryAmount getBuyNowPrice() { 54 return buyNowPrice; 55 } 56 57 public void setBuyNowPrice(MonetaryAmount buyNowPrice) { 58 this.buyNowPrice = buyNowPrice; 59 } 60 61 public MonetaryAmount getInitialPrice() { 62 return initialPrice; 63 } 64 65 public void setInitialPrice(MonetaryAmount initialPrice) { 66 this.initialPrice = initialPrice; 67 } 68 69 // ... 70 }
4.
1 package org.jpwh.model.advanced; 2 3 import java.io.Serializable; 4 import java.math.BigDecimal; 5 import java.util.Currency; 6 7 /* 8 This value-typed class should be <code>java.io.Serializable</code>: When Hibernate stores entity 9 instance data in the shared second-level cache (see <a href="#Caching"/>), it <em>disassembles</em> 10 the entity's state. If an entity has a <code>MonetaryAmount</code> property, the serialized 11 representation of the property value will be stored in the second-level cache region. When entity 12 data is retrieved from the cache region, the property value will be deserialized and reassembled. 13 */ 14 public class MonetaryAmount implements Serializable { 15 16 /* 17 The class does not need a special constructor, you can make it immutable, even with 18 <code>final</code> fields, as your code will be the only place an instance is created. 19 */ 20 protected final BigDecimal value; 21 protected final Currency currency; 22 23 public MonetaryAmount(BigDecimal value, Currency currency) { 24 this.value = value; 25 this.currency = currency; 26 } 27 28 public BigDecimal getValue() { 29 return value; 30 } 31 32 public Currency getCurrency() { 33 return currency; 34 } 35 36 /* 37 You should implement the <code>equals()</code> and <code>hashCode()</code> 38 methods, and compare monetary amounts "by value". 39 */ 40 public boolean equals(Object o) { 41 if (this == o) return true; 42 if (!(o instanceof MonetaryAmount)) return false; 43 44 final MonetaryAmount monetaryAmount = (MonetaryAmount) o; 45 46 if (!value.equals(monetaryAmount.value)) return false; 47 if (!currency.equals(monetaryAmount.currency)) return false; 48 49 return true; 50 } 51 52 public int hashCode() { 53 int result; 54 result = value.hashCode(); 55 result = 29 * result + currency.hashCode(); 56 return result; 57 } 58 59 /* 60 You will need a <code>String</code> representation of a monetary 61 amount. Implement the <code>toString()</code> method and a static method to 62 create an instance from a <code>String</code>. 63 */ 64 public String toString() { 65 return getValue() + " " + getCurrency(); 66 } 67 68 public static MonetaryAmount fromString(String s) { 69 String[] split = s.split(" "); 70 return new MonetaryAmount( 71 new BigDecimal(split[0]), 72 Currency.getInstance(split[1]) 73 ); 74 } 75 }
5.