考虑定制的映射类型
拿前面章节的的Address类的映射作为组件来举例:<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>
这个值类型映射很简单,用户自定义的新Java类型的所有属性都被映射到内建的SQL数据类型的单独的列。然而,也可以选择利用一个定制的映射类型把它当作一个简单的属性来映射:
<property name="homeAddress" type="auction.persistence.CustomAddressType"> <column name="HOME_STREET"/> <column name="HOME_CITY"/> <column name="HOME_ZIPCODE"/> </property>当然,在这个例子中,使用定制的映射类型替换组件映射的好处是令人置疑的。只要在加载和保存这个对象时不需要特殊的转化,现在必须编写的CustomAddressType就是额外的工作。然而,你可能已经看到定制的映射类型提供了一个额外的缓冲区——当在长期运行中需要额外的转化时,可能派得上用场的东西。
扩展点
Hibernate提供定义定制的映射类型时应用程序可能使用的几个接口。这些接口减少了创建新映射类型所涉及的工作,并使定制的类型免受Hibernate核心变化的影响。这帮助你轻松地升级Hibernate,并保留现有的定制映射类型。扩展点如下:
org.hibernate.usertype.UserType——基础的扩展点,用于多种情况。它为定制值类型实例的加载和存储提供基础的方法。
org.hibernate.usertype.CompositeUserType——包含比基础的UserType更多方法的一个接口,通常把有关值类型类的内部信息公开给Hibernate,例如单独的属性。然后可以在Hibernate查询中引用这些属性。
org.hibernate.usertype.UserCollectionType——很少被用来实现定制集合的接口。实现这个接口的定制映射类型不是在属性映射中声明,而是只对定制的集合映射有用。如果想要持久化一个非JDK的集合,并持久保持额外的主义时,就必须实现这个类型。
org.hibernate.usertype.EnhanceUserType——扩展了UserType并提供额外方法的接口,这些方法用来把值类型封送到XML表示法(或者从XML表示法中封送值类型),或者启用一个定制的映射类型,在标识符和辨别标志映射中使用。
org.hibernate.usertype.UserVersionType——扩展了UserType并提供额外方法的接口,这引起方法用于实体版本映射的定制映射类型。
org.hibernate.usertype.ParameterizedType——一个有用的接口,可以与所有其他的接口合并,来提供配置设置——也就是说,元数据中定义的参数。
定制映射类型的案例
Bid类定义了一个amount属性,Item类定义了一个initialPrice属性,这两者都是货币值。目前为止,我们只用了一个简单的BigDecimal来表示值,通过big_decimal映射到单个NUMBERIC列。假设你要在拍卖应用程序中支持多种货币,并且必须对这个(客户驱动的)变化重构现有的领域模型。
创建一个封装了货币和金额的新MonetaryAmount类。
import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; public class MonetaryAmount implements Serializable { private static final long serialVersionUID = -3446177576184667883L; private final BigDecimal amount; private final Currency currency; public MonetaryAmount(BigDecimal amount, Currency currency) { super(); this.currency = currency; this.amount = amount; } public BigDecimal getAmount() { return amount; } public Currency getCurrency() { return currency; } public boolean equals(Object obj) { if (null == obj) { return false; } if (obj == this) { return true; } if (obj.getClass() != this.getClass()) { return false; } MonetaryAmount ma = (MonetaryAmount) obj; return new EqualsBuilder().append(this.getAmount(), ma.getAmount()) .isEquals(); } public int hashCode() { final int PRIME = 31; return new HashCodeBuilder(this.getAmount().intValue(), PRIME) .toHashCode(); } }注意,必须实现equals()和hashCode()来完成类。
创建UserType
想象你正在使用一个用USD表示所有货币金额的遗留数据库。这个应用程序不再受限于单种货币(这是重构的重点),但是它要花费数据库团队一些时间进行改变。当持久化MonetaryAmount对象时,需要把金额转化为USD。当从数据库加载时,把它转化回用户根据其偏爱所选择的货币类型。package cn.jbit.hibernate.entity; import java.io.Serializable; import java.math.BigDecimal; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Currency; import org.hibernate.HibernateException; import org.hibernate.type.BigDecimalType; import org.hibernate.usertype.UserType; public class MonetaryAmountType implements UserType { //告诉Hibernate要使用什么SQL列类型生成DDL模式 public int[] sqlTypes() { return new int[] { BigDecimalType.INSTANCE.sqlType() }; } //映射Java值类型 @SuppressWarnings("unchecked") public Class returnedClass() { return MonetaryAmount.class; } //这个类型是否可变 public boolean isMutable() { return false; } //创建值的快照 public Object deepCopy(Object value) throws HibernateException { return value; } //以序列化的形保存信息的数据的高速缓存 public Serializable disassemble(Object value) throws HibernateException { return (Serializable) value; } //高速缓存的数据转变为MonetaryAmount的一个实例 public Object assemble(Serializable cached, Object owner) throws HibernateException { return cached; } //处理脱管对象状态的合并 public Object replace(Object original, Object target, Object owner) throws HibernateException { return original; } public boolean equals(Object x, Object y) throws HibernateException { if (x == y) return true; if (null == x || null == y) return false; return x.equals(y); } public int hashCode(Object x) throws HibernateException { return x.hashCode(); } //从JDBC的ResultSet获取属性值 public Object nullSafeGet(ResultSet resultSet, String[] names, Object owner) throws HibernateException, 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); } //把属性值写到JDBC的PreparedStatement public void nullSafeSet(PreparedStatement pstmt, Object value, int index) throws HibernateException, SQLException { if (null == value) { pstmt.setNull(index, BigDecimalType.INSTANCE.sqlType()); } else { MonetaryAmount anyCurrency = (MonetaryAmount) value; MonetaryAmount amountInUSD = MonetaryAmount.convert(anyCurrency, Currency.getInstance("USD")); pstmt.setBigDecimal(index, anyCurrency.getAmount()); } } }现在映射Item的initialPrice属性如下:
<property name = "initialPrice" column = "INITIAL_PRICE" type = "persistence.MonetaryAmountUserType"/>注意你把定制的用户类型放进了persistence包;它是应用程序的持久层的一部分,而不是领域模型或者业务层的一部分。
为了在注解中使用定制类型,必须添加一个Hibernate扩展:
@org.hibernate.annotations.Type( type = "persistence.MonetaryAmountUserType" ) @Column(name = "INITIAL_PRICE") private MonetaryAmount initialPrice;这是UserType可以执行的一种最简单的转化。更加复杂的事情也有可能。定制的映射类型可以执行验证;它可以把数据读取和写入到一个LDAP目录;它甚至可以从不同的数据库获取持久化对象。
简单的UserType实现的缺点在于,Hibernate不知道有关MonetaryAmount内部单独属性的任何信息。它所知的只是定制类型类和列名称。Hibernate查询引擎不知道如何查询amount或者特定的currency。
如果需要Hibernate查询的全部功能,就编写CompositeUserType。这个(稍微有点复杂的)接口把MonetaryAmount的属性公开给Hibernate查询。现在再次用这个更灵活的定制接口把它映射到两个列,实际上生成了组件映射的等价物。