首页 > 代码库 > 说说 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.Clobjava.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 来实现这个功能。注意,这里定义的顺序必须与实际情况相符!

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    说说 Hibernate 的映射策略