首页 > 代码库 > 说说 Hibernate 的映射策略
说说 Hibernate 的映射策略
1 基本属性映射
持久化类属性的 JPA 规则是:
- 持久化类的属性如果是基本类型或者基本类型的包装器,诸如
String, BigInteger, BigDecimal, java.util.Date, java.util.Calendar, java.sql.Date, java.sql.Time, java.sql.Timestamp, byte[], Byte[], char[], Character[]
,它们会被自动持久化。 - 如果一个类加了
@Embeddable
注解,表明这个类是属于其他某个类的一部分,它是内嵌的类,我们以后会说到。 - 如果属性类型是
java.io.Serializable
,那么它的值会被存储为序列化后的格式。 - 如果以上条件都不成立,那么 Hibernate 会在启动时抛出异常。
1.1 覆盖基本属性的默认值
如果某个属性不需要被持久化,可以加上 @javax.persistence.Transient
注解或者使用 java 的 transient
关键字。
默认情况下,所有的可持久化属性都是可为 null ,既是可选的。因此可以使用 @Basic
注解把某个属性改为必填,像这样:
@Basic(optional = false)
BigDecimal initialPrice;
这样配置以后,Hibernate 会在生成 SQL schema 时,把这个属性设置为非 null。如果在插入或者更新时,这个属性是 null,那么 Hibernate 就会抛出异常。
大多数工程师们更喜欢用 @Column
注解来声明非 null:
@Column(nullable = false)
BigDecimal initialPrice;
也就是说@Basic、@Column
以及之前所说的 Bean Validation 的 @NotNull
,它们的功能都一样,配置其中一个后,Hibernate 会对这个属性进行非 null 验证。建议使用 Bean Validation 的 @NotNull
,因为这样就能够在表现层手动验证一个 Item 实例。
关于 Bean Validation 的内容请参见 说说 Hibernate 领域模型与库表结构设计
@Column
也能够指定需要映射的表字段名:
@Column(name = "START_PRICE", nullable = false)
BigDecimal initialPrice;
@Column
还有一些属性设定,比如可以设定 catalog、schema 的名字,但是它们在实践中很少会被用到。
1.2 自定义存取属性的方式
可以选择是通过字段来直接存取,还是通过 getter/setter 方法来间接存取。
Hibernate 是依据持久化类的 @Id
注解来判断到底是采取哪种方式的。比如 @Id
放在某个属性上,那么所有的属性都会是通过字段来直接存取的。
JPA 规范中还提供了 @Access
注解,它有两个值,AccessType.FIELD
(通过字段来直接存取) 和 AccessType.PROPERTY
(通过 getter/setter 方法来间接存取)。可以把这个注解放在类上,这样就会应用于所有的属性,也可以放在某个类属性上,对它进行精细控制:
@Entity
public class Item {
@Id
@GeneratedValue(generator = Constants.ID_GENERATOR)
protected Long id;
@Access(AccessType.PROPERTY)
@Column(name = "ITEM_NAME")//Mapping are still expected here!
protected String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = !name.startsWith("AUCTION:") ? "AUCTION:" + name : name;
}
}
Hibernate 还有一个特性可以会很少用到,这里提一提。noop
级别的存取器。假设数据库表有一个 VALIDATED
字段,表示有效时间,但是没有把它放在领域类模型中(这种情况应该很少发生吧 O(∩_∩)O~)。为了能够对这个字段进行 SQL 查询,必须把它放在 hbm.xml 的 metadata 文件中:
<hibernate-mapping>
<class name="Item">
<id name="id">
...
</id>
<property name="validated"
column="VALIDATED"
access="noop"/>
</property>
</class>
</hibernate-mapping>
做了这样的映射以后,就能够在查询中使用 validated 咯。注意之前说过,如果使用了 Hibernate 的 hbm.xml 的 metadata 文件进行配置,那么所有在 Item 类上的持久化注解就都失效咯。
如果以上的映射还不能满足要求,那么可以直接自定义一个属性存取器!只要实现一个 org.hibernate.property.PropertyAccessor
接口,然后配置到下面这个注解中:
@org.hibernate.annotations.AttributeAccessor("my.custom.Accessor")
这是 Hibernate 4.3 + 新增的特性。
1.3 使用衍生出来的属性
这种属性的值是执行 SQL 语句后实时计算出来的,我们可以使用 @org.hibernate.annotations.Formula
注解:
@org.hibernate.annotations.Formula(
"substr (DESCRIPTION, 1, 12) || ‘...‘"
)
protected String shortDescription;
@org.hibernate.annotations.Formula(
" (select avg(b.AMOUNT) from BID b where b.ITEM_ID = ID)"
)
protected BigDecimal averageBidAmount;
当每次从数据库中获取 Item 后,这些属性就会被重新计算。Hibernate 会把这些属性放入到 select 查询中作为查询条件的一部分。
1.4 转换某列的值
假设表中有一个字段叫 IMPERIALWEIGHT
,它表示的重量,单位是磅。但是应用系统需要的重要单位是公斤,这就需要加入转换注解进行配置:
@Column(name = "IMPERIALWEIGHT")
@org.hibernate.annotations.ColumnTransformer(
read ="IMPERIALWEIGHT/ 2.20462",
write = "? * 2.20462"
)
protected double metricWeight;
这样配置后,就可以直接应用于 HQL:
List<Item> result = em.createQuery("select i from i where i.metricWeight = :w").setParameter("w",2.0).getResultList();
注意:这种方式生成的 SQL 语句可能无法直接利用数据库配置的索引。
1.5 配置属性的默认值
数据库可以配置某个字段的默认值,当插入新数据时,如果这个字段没有设置新值时,会把这个字段设置为默认值。
一般来说,当数据库自动为这个字段设置为默认值时,Hibernate 框架应该要更新相应的实体类实例才是。这可以通过配置 @org.hibernate.annotations.Generated annotation
注解来实现:
@Temporal(TemporalType.TIMESTAMP)
@Column(insertable = false, updatable = false)
@org.hibernate.annotations.Generated(
org.hibernate.annotations.GenerationTime.ALWAYS
)
protected Date lastModified;
@Column(insertable = false)
@org.hibernate.annotations.ColumnDefault("1.00")
@org.hibernate.annotations.Generated(
org.hibernate.annotations.GenerationTime.INSERT
)
protected BigDecimal initialPrice;
GenerationTime.ALWAYS
表示每次发生 新增或者更新 SQL 操作后,Hibernate 都会更新实例。在属性上也可以配置 @Column(insertable = false, updatable = false)
,这样这个属性就变成了只读属性了,也就是说这个属性只能通过数据库来生成咯。
@org.hibernate.annotations.ColumnDefault
用于配置默认值,这样 Hibernate 会在导出 schema DDL 的 SQL 时,加上字段默认值。
1.6 临时属性
有的属性,比如 timestamp 属性,会在插入数据后由数据库自动生成,然后它的值就不会再变化了:
@Temporal(TemporalType.TIMESTMAP)
@Column(updatable = false)
@org.hibernate.annotations.CreationTimestamp
protected Date createdOn;
//Java 8 API
protected Instant reviewedOn;
Hibernate 支持 /Java 8 中的 java.time
类包。
@Temporal
中的 TemporalType 的可选选项有 DATE、TIME、TIMESTAMP,表示的是这个属性是以何种时间字段类型存储在数据库中的。
这里如果没有配置 @Temporal
注解,Hibernate 会设置为 TemporalType.TIMESTMAP
。@org.hibernate.annotations.CreationTimestamp
插入时,是由数据库自动生成值,然后 Hibernate 自动刷新。还有一个 @org.hibernate.annotations.UpdateTimestamp
注解与 CreationTimestamp
注解类似,只不过它是检测数据更新时的变化。
1.7 映射枚举类型
假设需要一个拍卖类型:
public enum AuctionType{
HIGHEST_BID,
LOWEST_BID,
FIEXED_PRICE
}
可以这样映射:
@NotNull
@Enumerated(EnumType.AuctionType)
protected AuctionType auctionType = AuctionType.HIGHEST_BID;
@Enumerated
的默认值是 EnumType.ORDINAL
,这不好用,因为 ORDINAL 是枚举中元素的序列值(从 1 开始),这在设置默认值时,表现的不清晰。所以最好把它配置为
EnumType.AuctionType
。
2 映射嵌套类
假设我们的 User 类里面有两个属性(home、billing)都是 Address 类的类型,即它们都是 User 类的一部分(图中表现的是一种整体与部分的包含关系)。
2.1 数据库 schema
嵌套类 Address 没有自己的 ID,因为它必须属于某个持久化类,比如这里的 User 类。嵌套类的生命周期依赖于它的归属类,如果保存了归属类,那么它的嵌套类也会被保存。
2.2 建立嵌套类
@Embeddable//instead of @Entity
public class Address {
@NotNull//Ignored for DDL generation
@Column(nullable = false)//Used for DDL generation
protected String street;
@NotNull
@Column(nullable = false,length = 5)
protected String zipcode;
@NotNull
@Column(nullable = false)
protected String city;
//No-args constructor
protected Address(){
}
/**
* Convenience constructor
* @param street
* @param zipcode
* @param city
*/
public Address(String street, String zipcode, String city) {
this.street = street;
this.zipcode = zipcode;
this.city = city;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getZipcode() {
return zipcode;
}
public void setZipcode(String zipcode) {
this.zipcode = zipcode;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
}
- 一个嵌套类不会有唯一标识的属性。
- Hibernate 会调用无参的构造函数来创建一个实例。
- 可以加一个带参的构造函数,方便调用。
注意: 在嵌套类中定义的 @NotNull
属性,不会被 Hibernate 用于生成数据库的 schema。它只会被用于在运行时对 Bean 进行验证(Bean Validation)。所以我们要使用 @Column(nullable = false)
对 schema 进行控制。
下面是主类的代码:
@Entity
@Table(name = "USERS")
public class User implements Serializable {
@Id
@GeneratedValue(generator = Constants.ID_GENERATOR)
protected Long id;
public Long getId() {
return id;
}
//The Address is @Embeddable; no annotation is needed here.
protected Address homeAddress;
public Address getHomeAddress() {
return homeAddress;
}
public void setHomeAddress(Address homeAddress) {
this.homeAddress = homeAddress;
}
}
因为 Address 类是嵌套类,所以 Hibernate 会自动检测。
包含嵌套类的类,它的属性存取策略举例如下:
- 如果嵌套类的 @Entity 上配置了 field 存取机制——@Access(AccessType.FIELD),那么它的所有属性都使用 field 机制。
- 如果嵌套类的 @Entity 上配置了属性 存取机制——@Access(AccessType.PROPERTY),那么它的所有属性都使用属性存取机制。
- 如果主类在属性的 getter/setter 方法上加了 @Access(AccessType.PROPERTY),那么它就会使用属性存取机制。
- 如果把 @Access 配在主类上,那么就会依据配置来决定属性的存取策略。
当一个 User 没有 Address 信息时,会返回 null。
2.3 覆盖嵌套类的默认属性
假设 User 类还有一个 billingAdress 属性,它也是 Address 类型的,这就与之前的 homeAddress 属性发生冲突,所以我们要它进行覆盖处理:
@Embedded
@AttributeOverrides({
@AttributeOverride(name="street",column = @Column(name = "BILLING_STREET")),
@AttributeOverride(name = "zipcode",column = @Column(name =
"BILLING_ZIPCODE",length = 5)),
@AttributeOverride(name = "city",column = @Column(name = "BILLING_CITY"))
})
protected Address billingAddress;
public Address getBillingAddress() {
return billingAddress;
}
public void setBillingAddress(Address billingAddress) {
this.billingAddress = billingAddress;
}
@Embedded 在这里其实是多余的,它可能只在需要映射第三方包的类时,才有用。
@AttributeOverrides 会覆盖嵌套类中的配置,在示例中,我们把嵌套类中的三个属性都映射到了三个不同的字段。
2.4 映射多层嵌套类
假设我们定义了多个层级嵌套类,Address 是 User 的嵌套类,City 又是 Address 的嵌套类:
Address 类:
@Embeddable
public class Address {
@NotNull
@Column(nullable = false)
protected String street;
@NotNull
@AttributeOverrides(
@AttributeOverride(name = "name", column = @Column(name = "CITY", nullable = false))
)
protected City city;
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public City getCity() {
return city;
}
public void setCity(City city) {
this.city = city;
}
}
City 类:
@Embeddable
public class City {
@NotNull
@Column(nullable = false, length = 5)//Override VARCHAR(255)
protected String zipcode;
@NotNull
@Column(nullable = false)
protected String name;
@NotNull
@Column(nullable = false)
protected String country;
}
无论嵌套层级定义了有多深,Hibernate 都会理顺它们之间的关系。
可以在任何层级的类属性上加上 @AttributeOverrides ,覆盖默认的列名映射。这些层级都可以使用点语法,比如像这样:Address#City#name。
2.5 使用转换器实现从 Java 类型到 SQL 类型的映射
2.5.1 内建类型
2.5.1.1 原生与数字类型
名称 | Java 类型 | ANSI SQL 类型 |
---|---|---|
integer | int, java.lang.Integer | INTEGER |
long | long, java.lang.Long | BIGINT |
short | short, java.lang.Short | SMALLINT |
float | float, java.lang.Float | FLOAT |
double | double, java.lang.Double | DOUBLE |
byte | byte, java.lang.Byte | TINYINT |
boolean | boolean, java.lang.Boolean | BOOLEAN |
big_decimal | java.math.BigDecimal | NUMERIC |
big_integer | java.math.BigInteger | NUMERIC |
这里的名称指的是 Hibernate 定义的名称,它会在后面的自定义类型映射中用到。
Hibernate 会把 ANSI SQL 类型转换为实际配置的数据库方言类型。
NUMERIC 类型包含整数位数和精度,对于一个 BigDecimal 属性,它对应的类型是 NUMERIC(19,2)。可以使用 @Column 对整数位数和精度进行精细控制。
2.5.1.2 字符类型
名称 | Java 类型 | ANSI SQL 类型 |
---|---|---|
string | java.lang.String | VARCHAR |
character | char[], Character[], java.lang.String | CHAR |
yes_no | bollean, java.lang.Boolean | CHAR(1), ‘Y’ 或者 ‘N’ |
true_false | bollean, java.lang.Boolean | CHAR(1), ‘T’ 或者 ‘F’ |
class | java.lang.Class | VARCHAR |
locale | java.util.Locale | VARCHAR |
timezone | java.util.TimeZone | VARCHAR |
currency | java.util.Currency | VARCHAR |
使用 @Column(length=...)
或者 @Length
对属性进行标注后, Hibernate 会自动为属性选择最合适的字符串类型。比如 MySQL 数据库,如果长度在 65535 内,会选择 VARCHAR 类型,如果长度在 65536 ~ 16777215 之间,会选择 MEDIUMTEXT 类型,更长的属性会选择 LONGTEXT 类型。
2.5.1.3 日期和时间类型
名称 | Java 类型 | ANSI SQL 类型 |
---|---|---|
date | java.util.Date, java.sql.Date | DATE |
time | java.util.Date, java.sql.Time | TIME |
timestamp | java.util.Date, java.sql.Timestamp | TIMESTAMP |
calendar | java.util.Calendar | TIMESTAMP |
calendar_date | java.util.Calendar | DATE |
duration | java.time.Duration | BIGINT |
instant | java.time.Instant | TIMESTAMP |
localdatetime | java.time.LocalDateTime | TIMESTAMP |
localdate | java.time.LocalDate | DATE |
localtime | java.time.LocalTime | TIME |
offsetdatetime | java.time.OffsetDateTime | TIMESTAMP |
offsettime | java.time.OffsetTime | TIME |
zonedatetime | java.time.ZonedDateTime | TIMESTAMP |
列表中包含了 Java 8 的 java.time API,这是 Hibernate 独有的,它们并没有包含在 JPA 2.1 中。
如果存储了一个 java.util.Date
类型的值,获取时,该值类型会是 java.sql.Date
。这可能会在比较对象是否相等时发生问题。要把时间转换为 Unix 的毫秒时间数,使用这种方式进行比较:
aDate.getTime() > bDate.getTime()
特别是在集合中(诸如 HashSet)包含 java.util.Date, java.sql.Date|Time|Timestamp
,元素比较方法的差异。最好的方法就是保持类型的统一一致。
2.5.1.4 二进制类型以及大数据类型
名称 | Java 类型 | ANSI SQL 类型 |
---|---|---|
binary | byte[], java.lang.Byte[] | VARCHAR |
text | java.lang.String | CLOB |
clob | java.lang.Clob | CLOB |
serializable | java.io.Serializable | VARBINARY |
VARBINARY 类型是依赖于实际配置的数据库。
在 JPA 中有一个很方便的 @Lob:
@Entity
public class Item {
@Lob
protected byte[] image;
@Lob
protected String descripton;
}
这里会把 byte[] 映射为 BlOB 类型,把 String 映射为 CLOB 类型。可惜的是,这样无法实现懒加载大数据的字段。
所以建议使用 java.sql.Clob
和 java.sql.Blob
类型实现懒加载:
@Entity
public class Item {
@Lob
protected java.sql.Blob image;
@Lob
protected java.sql.Clob descripton;
}
甚至可以把大数据类型的字段通过字节流的方式来读取:
Item item = em.find(Item.class, ITEM_ID);
//You can stream the bytes directly...
InputStream imageDataStream = item.getImageBlob().getBinaryStream();
//or materialize them to memory:
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
StreamUtils.copy(imageDataStream, outputStream);
byte[] imageBytes = outputStream.toByteArray();
Hibernate 还提供了一些便利方法,比如直接从 InputStream 中一次性读取固定字节长度的数据用于持久化操作,这样做就不会消耗内存:
//Need the native Hibernate API
Session session = em.unwrap(Session.class);
//You need to know the number of bytes you want to read from the stream!
Blob blob = session.getLobHelper().createBlob(imageInputStream, byteLength);
someItem.setImageBlob(blob);
em.persist(someItem);
这里会把字节流的数据存储为 VARBINARY 类型的字段。
2.5.1.5 选择一个类型适配器
可以显式配置一个特殊的适配器注解:
@Entity
public class Item{
@org.hibernate.annotations.Type(type = "yes_no")
protected boolean verified = false;
}
这样配置后,会把 boolean 值转换为 Y 或者 N 后再进行存储。
2.5.2 创建一个自定义的 JPA 转换器
在我们的在线拍卖系统中,新增了一个需求,要求系统能够支持多种货币计算的功能。传统的做法是修改表结构,然后再修改相应的代码。另外一种方法是使用一个自定义的 JPA 转换器。
public class MonetaryAmount implements Serializable {
protected final BigDecimal value;
protected final Currency currency;
public MonetaryAmount(BigDecimal value, Currency currency) {
this.value = http://www.mamicode.com/value;"hljs-keyword">this.currency = currency;
}
public BigDecimal getValue() {
return value;
}
public Currency getCurrency() {
return currency;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MonetaryAmount that = (MonetaryAmount) o;
if (value != null ? !value.equals(that.value) : that.value != null) return false;
return !(currency != null ? !currency.equals(that.currency) : that.currency != null);
}
@Override
public int hashCode() {
int result = value != null ? value.hashCode() : 0;
result = 31 * result + (currency != null ? currency.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "MonetaryAmount{" +
"value="http://www.mamicode.com/ + value +
", currency=" + currency +
‘}‘;
}
public static MonetaryAmount fromString(String s) {
String[] split = s.split(" ");
return new MonetaryAmount(new BigDecimal(split[0]), Currency.getInstance(split[1]));
}
}
必须实现 equals() 和 hashCode() 方法,让 MonetaryAmount 的实例可以依据值进行比较。
2.5.2.1 转换基本属性
必须实现一个 toString() 方法,用于存储。fromString() 方法用于把数据从库表中加载后,在还原为 MonetaryAmount。这可以通过实现 AttributeConverter 接口来定义。
@Converter(autoApply = true)//Default for MonetaryAmount properties
public class MonetaryAmountConverter implements AttributeConverter<MonetaryAmount, String> {
@Override
public String convertToDatabaseColumn(MonetaryAmount attribute) {
return attribute.toString();
}
@Override
public MonetaryAmount convertToEntityAttribute(String dbData) {
return MonetaryAmount.fromString(dbData);
}
}
然后我们开始定义 MonetaryAmount 类型的属性:
@Entity
public class Item {
...
@NotNull
@Convert(//Optional:autoApply is enabled.
converter = MonetaryAmountConverter.class,
disableConversion = false
)
@Column(name = "PRICE", length = 63)
protected MonetaryAmount buyNowPrice;
}
@Convert 可以对转换器进行精细控制。
2.5.2.2 转换组件级别的属性
假设我们有一个抽象的 Zipcode 类,它有两个实现,一个是德国的邮政编码(5位数),一个是瑞士的邮政编码(4位数)。
Zipcode:
abstract public class Zipcode {
protected String value;
public Zipcode(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Zipcode zipcode = (Zipcode) o;
return !(value != null ? !value.equals(zipcode.value) : zipcode.value != null);
}
@Override
public int hashCode() {
return value != null ? value.hashCode() : 0;
}
}
GermanZipcode 类没有什么特别的:
public class GermanZipcode extends Zipcode {
public GermanZipcode(String value) {
super(value);
}
}
两个实现类的区别,我们放到转换器中去处理:
@Converter
public class ZipcodeConverter implements AttributeConverter<Zipcode, String> {
@Override
public String convertToDatabaseColumn(Zipcode attribute) {
return attribute.getValue();
}
@Override
public Zipcode convertToEntityAttribute(String s) {
if (s.length() == 5)
return new GermanZipcode(s);
//If you get to this point,consider cleaning up your database... or create an
// InvalidZipCode subclass and return it here.
throw new IllegalArgumentException("Unsupported zipcode in database: " + s);
}
}
接下来,就是配置上面定义的这个转换器啦:
@Entity
@Table(name = "USERS")
public class User implements Serializable {
...
@Convert(//Group multiple attribute conversions with @Converts
converter = ZipcodeConverter.class,
attributeName = "zipcode"//Or "city.zipcode" for nested embeddables
)
protected Address homeAddress;
}
这里的 @Convert 注解分为以下几种情况,来配置属性名称:
- 如果是
Map<Address, String>
结构,通过 key.zipcode,来配置属性名。 - 如果是
Map<String, Address>
结构,通过 value.zipcode,来配置属性名。 - 如果是
Map<Zipcode, String>
结构,通过 key,来配置属性名。 - 如果是
Map<String, Zipcode>
结构,通过 value,来配置属性名。
2.5.3 使用 UserTypes 来对 HIbernate 进行扩展
假设这样的场景,我们的 Item#buyNowprice 需要存储美元类型的货币值,而 Item#initialPrice 需要存储欧元。不要怀疑,现实的世界常常就是这么奇葩。但是标准的 JPA 不支持一次映射多个属性。所以我们要扩展。
2.5.3.1 扩展点
Hibernate 提供了以下这些扩展接口:
- UserType:内部使用 JDBC 的 PreparedStatement 和 ResultSet 进行转换。
- CompositeUserType:扩展了 UserType。
- ParameterizedUserType:提供了一些在映射方面的设置。
- DynamicParameterizedType:最强大的接口,可以动态写入映射表和字段。
- EnhancedUserType:用于适配主键级别的属性。
- UserVersionType:用于适配版本属性。
- UserCollectonType:用于适配自定义的集合属性。(很少用到)
2.5.3.2 自定义 UserType
public class MonetaryAmountUserType implements CompositeUserType, DynamicParameterizedType {
@Override
public String[] getPropertyNames() {
return new String[]{"value", "currency"};
}
@Override
public Type[] getPropertyTypes() {
return new Type[]{StandardBasicTypes.BIG_DECIMAL,
StandardBasicTypes.CURRENCY};
}
@Override
public Object getPropertyValue(Object component, int property) throws HibernateException {
MonetaryAmount monetaryAmount = (MonetaryAmount) component;
if (property == 0)
return monetaryAmount.getValue();
else
return monetaryAmount.getCurrency();
}
@Override
public void setPropertyValue(Object component, int property, Object value) throws HibernateException {
throw new UnsupportedOperationException("MonetaryAmount is immutable");
}
@Override
public Class returnedClass() {//Adapts class
return MonetaryAmount.class;
}
@Override
public boolean equals(Object x, Object y) throws HibernateException {
return x == y || !(x == null || y == null) && x.equals(y);
}
@Override
public int hashCode(Object x) throws HibernateException {
return x.hashCode();
}
/**
* Reads ResultSet
*
* @param rs
* @param names
* @param session
* @param owner
* @return
* @throws HibernateException
* @throws SQLException
*/
@Override
public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException {
BigDecimal amount = rs.getBigDecimal(names[0]);
if (rs.wasNull())
return null;
Currency currency = Currency.getInstance(rs.getString(names[1]));
return new MonetaryAmount(amount, currency);
}
/**
* Stores MonetaryAmount
*
* @param st
* @param value
* @param index
* @param session
* @throws HibernateException
* @throws SQLException
*/
@Override
public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
if (value =http://www.mamicode.com/= null) {
st.setNull(index, StandardBasicTypes.BIG_DECIMAL.sqlType());
st.setNull(index + 1, StandardBasicTypes.CURRENCY.sqlType());
} else {
MonetaryAmount amount = (MonetaryAmount) value;
MonetaryAmount dbAmount = convert(amount, convertTo);//When saving, convert to
// target currency
st.setBigDecimal(index, dbAmount.getValue());
st.setString(index + 1, convertTo.getCurrencyCode());
}
}
protected MonetaryAmount convert(MonetaryAmount amount, Currency toCurrency) {
return new MonetaryAmount(amount.getValue().multiply(new BigDecimal(2)), toCurrency);
}
@Override
public Object deepCopy(Object value) throws HibernateException {//Copies value
return value;
}
@Override
public boolean isMutable() {//Enables optimizations
return false;
}
/**
* Returns Serializable representation
*
* @param value
* @param session
* @return
* @throws HibernateException
*/
@Override
public Serializable disassemble(Object value, SessionImplementor session) throws HibernateException {
return value.toString();
}
/**
* Creates MonetaryAmount instance
*
* @param cached
* @param session
* @param owner
* @return
* @throws HibernateException
*/
@Override
public Object assemble(Serializable cached, SessionImplementor session, Object owner) throws HibernateException {
return MonetaryAmount.fromString((String) cached);
}
/**
* Returns copy of original
*
* @param original
* @param target
* @param session
* @param owner
* @return
* @throws HibernateException
*/
@Override
public Object replace(Object original, Object target, SessionImplementor session, Object owner) throws HibernateException {
return original;
}
protected Currency convertTo;
@Override
public void setParameterValues(Properties parameters) {
ParameterType parameterType = (ParameterType) parameters.get(PARAMETER_TYPE);
//Accesses dynamic parameters
String[] column = parameterType.getColumns();
String table = parameterType.getTable();
Annotation[] annotations = parameterType.getAnnotationsMethod();
String convertToParameter = parameters.getProperty("convertTo");
this.convertTo = Currency.getInstance
(convertToParameter != null ? convertToParameter : "USD");//Determines target
// currency
}
}
2.5.3.3 使用自定义 UserType
建议放在 package-info.java 中,这样就可以在包内任意使用啦 O(∩_∩)O~
@org.hibernate.annotations.TypeDefs({
@org.hibernate.annotations.TypeDef(
name = "monetary_amount_usd",
typeClass = MonetaryAmountUserType.class,
parameters = {@Parameter(name = "convertTo", value = http://www.mamicode.com/"USD")}
),
@org.hibernate.annotations.TypeDef(
name = "monetary_amount_eur",
typeClass = MonetaryAmountUserType.class,
parameters = {@Parameter(name = "convertTo", value = http://www.mamicode.com/"EUR")}
)
}) package net.deniro.hibernate.converter;
import org.hibernate.annotations.Parameter;
现在把自定义的类标注在对应的类属性上咯:
@Entity
public class Item {
private String id;
@Id
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@NotNull
@org.hibernate.annotations.Type(
type = "monetary_amount_usd"
)
@org.hibernate.annotations.Columns(columns = {
@Column(name = "BUYNOWPRICE_AMOUNT"),
@Column(name = "BUYNOWPRICE_CURRENCY", length = 3)
})
protected MonetaryAmount buyNowPrice;
@NotNull
@org.hibernate.annotations.Type(
type = "monetary_amount_eur"
)
@org.hibernate.annotations.Columns(columns = {
@Column(name = "BUYNOWPRICE_AMOUNT"),
@Column(name = "BUYNOWPRICE_CURRENCY", length = 3)
})
protected MonetaryAmount initialPrice;
}
因为 JPA 不支持多个 @Column 注解映射到一个属性上,所以我们要使用 @org.hibernate.annotations.Columns
来实现这个功能。注意,这里定义的顺序必须与实际情况相符!
说说 Hibernate 的映射策略