>>分享JPA、Hibernate、Mybatis对象持久化技术,对《精通JPA与Hibernate:Java对象持久化详解》提供技术支持 书籍支持  卫琴直播  品书摘要  在线测试  资源下载  联系我们
发表一个新主题 开启一个新投票 回复文章 您是本文章第 23104 个阅读者 刷新本主题
 * 贴子主题:  盘点Hibernate的值类型映射技巧 回复文章 点赞(0)  收藏  
作者:flybird    发表时间:2020-02-17 08:26:58     消息  查看  搜索  好友  邮件  复制  引用

映射基本属性

基本类型映射

基本类型的映射规则如下:

如果属性是基元或者基元包装器,如String, BigInteger, BigDecimal, Date, Calendar, byte[], char[],则它们会被自动持久化。
如果属性所属的类标记为@Embeddable,那么Hibernate会将其映射为多列,稍后再说。
如果属性的类是java.io.Serializable,那么Hibernate会将其值以序列化的方式存储,此时存储的是一系列字节(很蠢)。

@Column注解

此注解会更改属性的默认设置,通常用作给属性添加NOT NULL约束,
或者设置映射到数据库表的列的名称,用法如下:

@Column(name = "PRICE", nullable = false)
protected BigDecimal price;


此时的price属性将会映射到表中名为”PRICE”的列,且试图添加一个price属性为null的记录将会得到异常。

此外要和Bean Validation中的@NotNull注解区分开来,此注解在运行时检查所标注的属性是否为空,并不能在SQL中为相应的列添加NOT NULL注解。

自定义属性访问

在默认情况下,如果一个持久化类的@Id注解位于字段上,那么之后Hibernate存储和加载类实例时会直接访问字段。如果@Id位于get方法上,那么Hibernate之后会通过访问器间接访问字段。

如果要修改默认设置,那么可以通过@Access注解来标记字段或访问方法。

@Entity
public class Item {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    protected Long id;

    @Access(AccessType.PROPERTY)
    @Column(name = "ITEM_NAME", nullable = false)
    protected String name;

    public String getName() {
        return name;
    }
}

此后对name属性的更改就会通过访问器的方式,而非直接访问该字段。

派生属性

采用@org.hibernate.annotations.Formula注解来声明派生属性,派生属性在持久化类被检索的时候根据SQL语句进行估算,如下所示:

@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;

注意的是,估算的属性是只读的,其只能出现在select语句当中,绝不会出现在update和insert当中。

转换列值

假设数据库表中有一个YUAN的数据库列,现在将其映射到持久化类的属性当中,我们希望能够以分的形式来呈现。当然,在数据检索出来之后手动转换是一个不错的选择,但是我们可以利用
@org.hibernate.annotations.ColumnTransformer注解进行列转换,如下所示:

@Column(name = "YUAN")
@org.hibernate.annotations.ColumnTransformer(
        read = "YUAN * 10.0",
        write = "? / 10.0"
)
protected BigDecimal price;

生成默认值

有关生成默认值的博文地址如下:hibernate之生成的和默认的属性值(使用generated刷新实体),不过经过笔者的实践,这个貌似在MySQL环境当中存在着问题,具体请参见:Hibernate @Generated annotation doesn’t work well

枚举映射

可以通过以下方式映射一个枚举属性:

public enum Type {
    BOOK,
    CAR,
    FOOD
}

@NotNull
@Enumerated(EnumType.STRING)
protected Type type;

type会以字符串的形式存到数据库表当中。

映射可嵌入组件

可以用@Embeddable注解声明一个类是可嵌入的,可嵌入的类的每个属性将会被映射到外层持久化类所映射的表的单个列当中。举个例子,想要在USER表当中存储关于用户的信息,其中有其地址信息,地址可以作为一个可嵌入类来定义:

@Embeddable
public class Address {
    @Column(nullable = false)
    protected String city;

    @Column(nullable = false)
    protected String zipCode;

    @Column(nullable = false)
    protected String street;

    public Address() {
    }

    public Address(String city, String zipCode, String street) {
        this.city = city;
        this.zipCode = zipCode;
        this.street = street;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getZipCode() {
        return zipCode;
    }

    public void setZipCode(String zipCode) {
        this.zipCode = zipCode;
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }
}

User定义:

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    protected Long id;

    @Column(nullable = false)
    String name;

    protected Address address;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    public Long getId() {
        return id;
    }
}

此时USER表的结构是这样的:

SHOW CREATE TABLE USER;

CREATE TABLE `USER` (
  `id` bigint(20) NOT NULL,
  `city` varchar(255) NOT NULL,
  `street` varchar(255) NOT NULL,
  `zipCode` varchar(255) NOT NULL,
  `name` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=gbk

重写嵌入属性

可以通过以下方式重写嵌入的属性:

@AttributeOverrides({
        @AttributeOverride(name = "street", column = @Column(name = "USER_STREET")),
        @AttributeOverride(name = "zipCode", column = @Column(name = "USER_ZIP_CODE")),
        @AttributeOverride(name = "city", column = @Column(name = "USER_CITY"))
})
protected Address address;

@AttributeOverride当中name对应的是Address的属性名,column对应的是要重新映射的列的信息。

转换器

我们已经知道在映射java.lang.String等基本类型的时候,Hibernate会将其映射到SQL基本类型,除了基本内置类型之外,可以定义转换器和用户类型。

内置类型

常见的内置类型如下:


名称               Java类型                              ANSISQL类型

integer           java.lang.Integer                       INTEGER
long               java.lang.Long                            BIGINT
float               java.lang.Float                            FLOAT
double           java.lang.Double                        DOUBLE
boolean         java.lang.Boolean                      BOOLEAN
big_decimal  java.lang.BigDecimal                 NUMERIC
big_integer   java.lang.BigInteger                   NUMERIC
String            java.lang.String                           VARCHAR
yes_no          java.lang.Boolean                       CHAR(1), ‘Y’ or ‘N’
class             java.lang.Class                              VARCHAR
currency       java.util.Currency                         VARCHAR
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

名称为Hibernate特有,用于自定义类型映射,如:
@org.hibernate.annotations.Type(type = "yes_no")
protected boolean verified;


现在VERIFIED所对应的SQL类型应该为CHAR(1),且其值为’Y’或’N’。

JPA转换器

可以使用JPA原生转换器来完成自定义类型到另一个基本类型的转换,如下所示:

MonetaryAmount类:

public class MonetaryAmount implements Serializable {
    static final long serialVersionUID = 1L;

    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.equals(that.value)) return false;
        return currency.equals(that.currency);
    }

    @Override
    public int hashCode() {
        int result = value.hashCode();
        result = 31 * result + currency.hashCode();
        return result;
    }

    @Override
    public String toString() {
        return value + " " + currency;
    }

    public static MonetaryAmount fromString(String str) {
        String[] split = str.split(" ");
        return new MonetaryAmount(new BigDecimal(split[0]), Currency.getInstance(split[1]));
    }
}


MonetaryAmountConverter:
@Converter(autoApply = true)
public class MonetaryAmountConverter implements AttributeConverter<MonetaryAmount, String> {

    @Override
    public String convertToDatabaseColumn(MonetaryAmount monetaryAmount) {
        return monetaryAmount.toString();
    }

    @Override
    public MonetaryAmount convertToEntityAttribute(String s) {
        return MonetaryAmount.fromString(s);
    }
}

则在持久化类里面可以这样定义:

@NotNull
//可选,因为设置了autoApply
@Convert(converter = MonetaryAmountConverter.class)
@Column(name = "PRICE")
protected MonetaryAmount ma;

此时Hibernate便会先将此属性转成相应的字符串再将其存到数据库中。

UserType扩展

JPA转换器存在诸多限制。其不支持单值到多个列的转换,另一个限制是与查询引擎的集成,不能够在HQL查询语句当中直接访问被转换类型的属性,如:
“select i from Item i where i.ma.value > 100”。如果需要更加灵活的扩展,那就需要Hibernate提供的扩展接口。

可以在org.hibernate.usertype包下找到合适的扩展接口,常见接口如下:
UserType——可以通过与底层JDBC的PreparedStatement和ResultSet交互来转换值。
CompositeUserType——扩展了上述的UserType接口,提供了更多与适配类的相关的信息。例如,可以告知MonetaryAmount有两个属性:value, currency,然后就可以在select查询语句当中引用这些属性。
ParameterizedUserType——提供对适配器参数的设置。
DynamicParameterizedUserType——允许访问适配器的动态信息,如映射列和表名称,可以替代ParameterizedUserType。

定义用户类型

如下定义了MonetaryAmount类的UserType,如下所示:

public class MonetaryAmountType implements CompositeUserType {

    // 获取类内部属性的名字
    @Override
    public String[] getPropertyNames() {
        System.out.println("getPropertyNames() called");
        return new String[]{ "value", "currency" };
    }

    // 得到属性的类型
    @Override
    public Type[] getPropertyTypes() {
        System.out.println("getPropertyTypes() called");
        return new Type[]{ StandardBasicTypes.BIG_DECIMAL, StandardBasicTypes.CURRENCY };
    }

    // 获取指定位置上属性的值
    @Override
    public Object getPropertyValue(Object o, int i) throws HibernateException {
        System.out.println("getPropertyValue() called, and i is " + i);
        MonetaryAmount ma = (MonetaryAmount) o;
        if (i == 0)
            return ma.getValue();
        return ma.getCurrency().toString();
    }

    @Override
    public void setPropertyValue(Object o, int i, Object o1) throws HibernateException {
        throw new UnsupportedOperationException("setProperty() cannot be called");
    }

    // 返回要转化的类型
    @Override
    public Class returnedClass() {
        System.out.println("returnedClass() called");
        return MonetaryAmount.class;
    }

    // 判断两对象是否相同
    @Override
    public boolean equals(Object o, Object o1) throws HibernateException {
        System.out.println("equals() called");
        return o.equals(o1);
    }

    @Override
    public int hashCode(Object o) throws HibernateException {
        System.out.println("hashCode() called");
        return o.hashCode();
    }

    // 查询的结果集到相应类的转变
    @Override
    public Object nullSafeGet(ResultSet resultSet, String[] strings, SharedSessionContractImplementor sharedSessionContractImplementor, Object o) throws HibernateException, SQLException {
        System.out.println("nullSafeGet() called");
        if (resultSet.wasNull())
            return null;
        BigDecimal value = resultSet.getBigDecimal(strings[0]);
        Currency currency = Currency.getInstance(resultSet.getString(strings[1]));
        return new MonetaryAmount(value, currency);
    }

    @Override
    public void nullSafeSet(PreparedStatement preparedStatement, Object o, int i, SharedSessionContractImplementor sharedSessionContractImplementor) throws HibernateException, SQLException {
        System.out.println("nullSafeSet() called and i is " + i);
        if (o == null) {
            preparedStatement.setNull(i, StandardBasicTypes.BIG_DECIMAL.sqlType());
            preparedStatement.setNull(i + 1, StandardBasicTypes.CURRENCY.sqlType());
        } else {
            MonetaryAmount ma = (MonetaryAmount) o;
            preparedStatement.setBigDecimal(i, ma.getValue());
            preparedStatement.setString(i + 1, ma.getCurrency().toString());
        }
    }

    // 用于获取值的副本,不可变对象返回本身即可
    @Override
    public Object deepCopy(Object o) throws HibernateException {
        return o;
    }

    // 如果对象是不可变的,Hibernate会采取优化措施
    @Override
    public boolean isMutable() {
        return false;
    }

    // 返回序列化表示的值
    @Override
    public Serializable disassemble(Object o, SharedSessionContractImplementor sharedSessionContractImplementor) throws HibernateException {
        System.out.println("disassemble() called");
        return o.toString();
    }

    // 用序列化的值创造对象
    @Override
    public Object assemble(Serializable serializable, SharedSessionContractImplementor sharedSessionContractImplementor, Object o) throws HibernateException {
        System.out.println("assemble() called");
        return MonetaryAmount.fromString((String) serializable);
    }

    // 在EntityManager#merge()操作期间调用,需要返回原始对象的副本
    @Override
    public Object replace(Object o, Object o1, SharedSessionContractImplementor sharedSessionContractImplementor, Object o2) throws HibernateException {
        return o;
    }
}

以上代码有注释,不再细说。

使用用户类型

使用用户类型时,可以先用包级注释声明类型:

@org.hibernate.annotations.TypeDefs(
    @org.hibernate.annotations.TypeDef(name = "monetary_amount_type",
            typeClass = com.stephen.hibernatepractice.converter.MonetaryAmountType.class)
)
package com.stephen.hibernatepractice;

在实体类中引用:

@NotNull
@org.hibernate.annotations.Type(type = "monetary_amount_type")
@org.hibernate.annotations.Columns(columns = {
        @Column(name = "PRICE"),
        @Column(name = "CURRENCY", length = 3)
}
)
protected MonetaryAmount monetaryAmount;

则monetaryAmount属性的value映射为PRICE列,currency映射为CURRENCY列,并且此时可以在HQL select语句里面引用monetaryAmount属性的属性。



程序猿的技术大观园:www.javathinker.net

[这个贴子最后由 flybird 在 2020-02-23 11:26:52 重新编辑]
  Java面向对象编程-->数组
  JavaWeb开发-->JavaWeb应用入门(Ⅰ)
  JSP与Hibernate开发-->通过JPA API检索数据
  Java网络编程-->ServerSocket用法详解
  精通Spring-->绑定表单
  Vue3开发-->虚拟DOM和render()函数
  什么是XA事务
  孙卫琴的《精通JPA与Hibernate》的读书笔记:比较JPA的Entit...
  十分钟入门 JPA
  推荐:Spring,JPA与Hibernate的最新整合范例讲解(孙卫琴主讲...
  用注解和XML描述对象-关系映射的区别,和优缺点
  理解事务的四种隔离级别
  用CriteriaBuilder进行动态查询
  Hibernate中通过FetchProfile的方式实现动态数据获取
  EntityTransaction 与UserTransaction的区别
  JPA @Convert注解的用法
  Hibernate的八种对象标识符生成策略
  JPA API入门
  Hibernate5源码解析:SessionFactroy的创建过程
  总结Hibernate5的新特性
  在看孙老师的hibernate书。。。
  更多...
 IPIP: 已设置保密
楼主      
1页 0条记录 当前第1
发表一个新主题 开启一个新投票 回复文章


中文版权所有: JavaThinker技术网站 Copyright 2016-2026 沪ICP备16029593号-2
荟萃Java程序员智慧的结晶,分享交流Java前沿技术。  联系我们
如有技术文章涉及侵权,请与本站管理员联系。