说说 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 类型
integerint, java.lang.IntegerINTEGER
longlong, java.lang.LongBIGINT
shortshort, java.lang.ShortSMALLINT
floatfloat, java.lang.FloatFLOAT
doubledouble, java.lang.DoubleDOUBLE
bytebyte, java.lang.ByteTINYINT
booleanboolean, java.lang.BooleanBOOLEAN
big_decimaljava.math.BigDecimalNUMERIC
big_integerjava.math.BigIntegerNUMERIC

这里的名称指的是 Hibernate 定义的名称,它会在后面的自定义类型映射中用到。

Hibernate 会把 ANSI SQL 类型转换为实际配置的数据库方言类型。

NUMERIC 类型包含整数位数和精度,对于一个 BigDecimal 属性,它对应的类型是 NUMERIC(19,2)。可以使用 @Column 对整数位数和精度进行精细控制。

2.5.1.2 字符类型
名称Java 类型ANSI SQL 类型
stringjava.lang.StringVARCHAR
characterchar[], Character[], java.lang.StringCHAR
yes_nobollean, java.lang.BooleanCHAR(1), ‘Y’ 或者 ‘N’
true_falsebollean, java.lang.BooleanCHAR(1), ‘T’ 或者 ‘F’
classjava.lang.ClassVARCHAR
localejava.util.LocaleVARCHAR
timezonejava.util.TimeZoneVARCHAR
currencyjava.util.CurrencyVARCHAR

使用 @Column(length=...) 或者 @Length 对属性进行标注后, Hibernate 会自动为属性选择最合适的字符串类型。比如 MySQL 数据库,如果长度在 65535 内,会选择 VARCHAR 类型,如果长度在 65536 ~ 16777215 之间,会选择 MEDIUMTEXT 类型,更长的属性会选择 LONGTEXT 类型。

2.5.1.3 日期和时间类型
名称Java 类型ANSI SQL 类型
datejava.util.Date, java.sql.DateDATE
timejava.util.Date, java.sql.TimeTIME
timestampjava.util.Date, java.sql.TimestampTIMESTAMP
calendarjava.util.CalendarTIMESTAMP
calendar_datejava.util.CalendarDATE
durationjava.time.DurationBIGINT
instantjava.time.InstantTIMESTAMP
localdatetimejava.time.LocalDateTimeTIMESTAMP
localdatejava.time.LocalDateDATE
localtimejava.time.LocalTimeTIME
offsetdatetimejava.time.OffsetDateTimeTIMESTAMP
offsettimejava.time.OffsetTimeTIME
zonedatetimejava.time.ZonedDateTimeTIMESTAMP

列表中包含了 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 类型
binarybyte[], java.lang.Byte[]VARCHAR
textjava.lang.StringCLOB
clobjava.lang.ClobCLOB
serializablejava.io.SerializableVARBINARY

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 = value;
        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=" + 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 == 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 = "USD")}
        ),
        @org.hibernate.annotations.TypeDef(
                name = "monetary_amount_eur",
                typeClass = MonetaryAmountUserType.class,
                parameters = {@Parameter(name = "convertTo", value = "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 来实现这个功能。注意,这里定义的顺序必须与实际情况相符!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值