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

时间: 2024-12-30 15:32:25

说说 Hibernate 的映射策略的相关文章

【SSH快速进阶】——Hibernate继承映射:每个具体类映射一张表

上篇文章说的是每个类映射一张表,不管是父类还是子类.与上篇文章不同的是,这里是每个"具体类"映射一张表,什么意思呢?就是让每个子类(具体的.有意义的类)映射一张表. 场景 与上篇文章场景一样,如下类图 上图中Pig类和Bird类继承Animal类,要让每个具体类映射一张表,就是只映射Pig和Bird,如下表所示: (表 1) 上面的表有个特点就是,t_pig和t_bird的主键永远都不会相同.因为表面上看起来这是两张表,但实际上存储的都是动物(同一类型),所以还可以看做是一张表. 配置

hibernate抓取策略学习

一.hibernate抓取策略概述 Hibernate抓取策略(fetching strategy)是指:当应用程序需要在(Hibernate实体对象图的)关联关系间进行导航的时候, Hibernate如何获取关联对象的策略.抓取策略可以在O/R映射的元数据中声明,也可以在特定的HQL 或条件查询(Criteria Query)中重载声明. 需要注意的是:hibernate的抓取策略只影响get load 方法,对hql是不影响的. 二.hibernate 抓取策略分类 hibernate有如下

hibernate 继承映射

对于面向对象的程序设计语言而言,继承和多态是两个最基本的概念.Hibernate 的继承映射可以理解持久化类之间的继承关系.例如:人和学生之间的关系.学生继承了人,可以认为学生是一个特殊的人,如果对人进行查询,学生的实例也将被得到. Hibernate支持三种继承映射策略: 使用 subclass 进行映射:将域模型中的每一个实体对象映射到一个独立的表中,也就是说不用在关系数据模型中考虑域模型中的继承关系和多态. 使用 joined-subclass 进行映射: 对于继承关系中的子类使用同一个表

Hibernate基础映射

在说Hibernate映射前,我们先来了解下对象关系映射 ORM.ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现.这样开发人员就可以把对数据库的操作转化为对这些对象的操作.我们来看一张图 通过该图,我们可以看出业务实体,在数据库中表现为关系数据,而在内存中表现为对象.应用程序处理对象很容易,但是很难处理关系数据.ORM做到了关系数据和对象数据之间的映射,可以通过映射关系自动产生SQL语句,在业务逻辑层和数据层之间充当桥梁. Hibernate映射 Hibernate文件

JPA实体继承实体的映射策略

注:这里所说的实体指的是@Entity注解的类 继承映射使用@Inheritance来注解,它的strategy属性的取值由枚举InheritanceType来定义(包括SINGLE_TABLE.TABLE_PER_CLASS.JOINED,分别对应三种继承策略).@Inheritance注解只能作用于继承结构的超类上.如果不指定继承策略,默认使用SINGLE_TABLE. JPA提供了三种继承映射策略: 1. 一个类继承结构一个表的策略.这是继承映射的默认策略.即如果实体类B继承实体类A,实体

hibernate之映射文件VS映射注解

前言 对于java开发者而言,注解应该不是一个陌生的概念,早在JavaSE阶段,例如@Override标记重写父类方法或实现接口方法,@Test标记单元测试方法,所以我们可以简单地把它理解为一种有特殊含义的标记...在开发过程中,我们还可以用注解方式替代配置文件实现相关功能,例如Java web开发中,3.0版本后,利用@WebServlet.@WebListener等注解分别可以替代web项目XML配置文件中相关内容.而本文中讲述的就是Hibernate的映射配置文件与映射注解的对比,这两种方

【SSH快速进阶】——Hibernate继承映射:每棵继承树映射一张表

我们都知道,Hibernate最大的一个优点就是使开发更加"面向对象",类与类之间有继承关系,Hibernate中也对这种继承关系提供了映射的封装. Hibernate为继承映射提供了三种策略 1.每棵继承树使用一张表 2.每个子类使用一张表 3.每个具体类使用一张表 本文对第一种策略进行说明. 场景 如下类图 上图中Pig类和Bird类继承Animal类,每棵继承树对应一张表,即在同一棵继承树中,所有的类的对象信息(记录)共同存放到一张表中,要判断某条记录属于哪个对象,需要在表中添加

SSH开发实践part3:hibernate继承映射

0 大家好.上次讲了关于hibernate中双向1-N的映射配置,可以参考:http://www.cnblogs.com/souvenir/p/3784510.html 实际项目中,对象间的关系比较复杂,除了上次讲的相互关联以外,这次我们要讲的就是关于对象的继承.hibernate如何来通过配置完成对象的继承? 1 比如有一个父类person,然后两个对应的子类,一个是teacher,一个是student.教师和老师除了拥有person这个类所有的属性以外,还会有一些自己独特的属性. hiber

浅谈Hibernate关系映射(2)

继上篇博客 一对一关系映射:一对一关联映射在实际生活中是比较常见的,如人与身份证的关系,通过人这个对象可以找到他相关的内容. 一对一单向(主键): 单向一对一主键关联,靠的是它们的主键相等,从Person中能看到IdCard,也就是把t_idCard中的主键拿过来当做t_Pseron的主键. 如图的线表示一个关联,在person中可以看见idcard.即在person中持有idCard的引用 person类的映射关系 <hibernate-mapping> <class name=&qu