在上一章解决了映射Entity类最基本的东西, 即主键还有一些命名的问题. 剩下就要来解决映射值类型.

在一个Entity类里除了关键的和Entity必须搭配出现的@Id之外, 剩下的有很多都是值类型, 就来看看如何映射这些类型.

要映射的值分成两大类, 一类是Java的基本类型, 还有一类就是自定义类型, 在Java中自定义类型就是自定义的类. 像Address对象这样的一个类如果映射成值类型, 应该如何操作.

由于所有的持久化对象要么映射成Entity, 要么映射成value type, 因此看完这章之后, 最基本的映射就了解了.

  1. 基础类型映射规则
  2. 控制默认映射规则 – @Transient @Basic @Column @Access
  3. 派生值 – @Formula
  4. 列转换器 – @ColumnTransformer
  5. 默认值 – 数据库生成/强制默认值/普通默认值
  6. 时间类型 – @Temporal
  7. 枚举类型 – @Enumerated

基础类型映射规则

在映射一个持久化类的时候, 无论映射成Entity还是Embedded类型(后边会学), 持久化类的所有属性默认都会被持久化. JPA对于所有属性默认的规则如下:

  1. 如下的Java类型或者包装器类型, 都会被自动持久化, Hibernate会用对应的SQL类型和与属性相同的列名称在数据库中存取值:
    • String
    • int 系列
    • BigDecimal
    • java.util.Date
    • java.util.Calendar
    • java.sql.Date
    • java.sql.Time
    • java.sql.Timestamp
    • byte[]
    • Byte[]
    • char[]
    • Character[]
  2. 将一个类或者一个成员变量加上@Embeddable注解, Hibernate会将这个类映射成为宿主类的内嵌对象, 晚点就会看到Address类是如何被映射的. 用于
  3. 如果一个属性的类型是java.io.Serializable, 则数据会被以二进制形式存在数据库中, 一般不要使用这种类型的属性
  4. Hibernate在上述规则都无法匹配一个属性或者一个对象的引用的时候, 就会报异常.

上述规则使得我们无需针对每一个类型进行具体配置, 仅仅只有出现异常或者想要具体控制的属性才需要具体配置. 下边就来看看一些控制具体配置的方法.

自定义控制映射规则

不持久化某些属性

首先就是要看看如果不持久化一个属性要如何标记出来.

很多时候未必持久化全部的属性, 比如一些属性在运行时生成或者计算所得, 没有必要将其持久化, 可以采取如下方法(目前都是基于@Id注解在属性上让Hibernate通过反射直接存取字段):

  1. @javax.persistence.Transient注解
  2. 这个属性使用了Java的transient关键字
  3. @Basic(optional = false)注解

覆盖默认名称和是否可以为空的设置

在默认的情况下, 一个持久化属性按照名称全小写, 然后是optional 即可以为空的方式进行持久化, 观看生成的SQL语句就可以知道这个规则.Java的命名采取驼峰样式, 所以一个aProperty的默认持久化列名是全小写.

可以使用如下几个注解来覆盖默认的名称和是否可以为空

  1. @Basic(optional = false), 这个注解用来将属性标明不可为空, 生成的SQL语句会带上 not null约束. 不过这个注解因为只有两个属性, 可用范围太小, 一般不用
  2. @Column(nullable = false, name = "saner"), 这个注解非常通用, 推荐用这个替代@Basic, 可以控制具体的列名, 是否为空, 还可以配置catalog和schema等信息.
  3. @NotNull,来自validation 标准的注解, 如果为空是报异常.

控制通过字段还是方法存取属性

在之前一直都是@Id标记在属性上, 而且没有提供其他配置, 这样一个类及其所属的Embedded类型都会使用这个类的访问配置, 即通过反射直接存取.

如果要配置具体通过字段还是方法存取属性, 可以在两个层面进行设置:

  1. 类层面, 将@Access(AccessType.PROPERTY/FIELD)加在类上, 表示这个类的所有默认设置, PROPERTY表示运行时通过方法存取, FIELD表示通过反射存取. 在配置了类的策略之后, 还可以把@Access加在具体的属性或者方法上来表示某个字段的具体设置.
    实际上, 单独使用@Id放在属性上, 就和默认使用@Access(AccessType.FIELD)相同.
  2. @Access加在属性上, 表示覆盖类的设置, 单独配置这个属性. 如果配置成PROPERTY, 在类中要编写符合规范的访问器方法.

一个简单的例子如下:

import javax.persistence.*;

@Entity
@Table
public class Message {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;

    @Column(nullable = false, name="saner")
    @Access(AccessType.PROPERTY)
    private String text;

    public Message() {

    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }
}

这里没有在类上配置@Access注解, 然后看到@Id配置在成员变量上, 那么这个类的默认策略就是通过反射访问属性. 然后@Access(AccessType.PROPERTY)配置在text属性上, 说明对text属性的存取通过get/set方法来操作. 所以还需要配上对应的方法.

假如Message类中还有一个没有进行任何标记的成员变量, 这个成员变量的访问方式也是按照类的默认策略, 即反射访问属性.

注意, 如果类加上了@Access(AccessType.PROPERTY)注解, 所有的注解需要移动到getter方法上, 哪怕是指定这个属性需要通过反射访问的@Access(AccessType.FIELD)也需要加在getter方法上.

没有特殊需求的情况下, 使用反射方式足够. 但是这里要知道通过方法访问的特色, 就是之前提过, 未必需要一个实际的成员变量, 而是让Hibernate只要看到有对应的getter/setter方法就认为有一个字段, 看这个例子:

import javax.persistence.*;

@Entity
@Table
@Access(AccessType.PROPERTY)
public class Message {

    private long id;

    private String text;

    public Message() {

    }

    @Column(nullable = false, name="saner")
    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getFullName() {
        return this.text;
    }

    public void setFullName(String text) {
        if (text == null) {
            this.text = "NONAME";
        } else {
            this.text = text + "set";
        }
    }

    public String getFullName(String text) {
        return this.text;
    }

        @Override
    public String toString() {
        return "Message{" +
                "id=" + id +
                ", text='" + text + '\'' +
                '}';
    }
}

这个类在生成SQL语句的时候如下:

Hibernate:
    create table Message (
       id int8 not null,
        fullName varchar(255),
        saner varchar(255) not null,
        primary key (id)
    )

Message类中全部属性都通过方法访问, 而不是字段, setFullName和getFullName就刻画出了一个可以被持久化但是实际上存在的成员变量. 如果这里将类策略改成通过字段访问(@Id要挪到成员变量上), 持久化的时候就不出现fullname这个属性了.

@Formula注解

Hibernate提供了@Formula注解, 被注解的属性不会进行持久化, 而是每次查询这个类的时候, 会执行@Formula中的SQL语句, 将结果写入到这个属性上. 不过这个仅发生在查询的时候, 所以可能值会过期.

Formula中如果写SELECT的话, 是一个子查询, 需要搭配其他的来使用. 如果写表达式的话, 就是一个值可以生成, 比如PgSQL中的那些函数.

@Entity
@Table
public class Message {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;

    private String text;

    @org.hibernate.annotations.Formula("CURRENT_DATE")
    private Date currentDate;

    public Date getCurrentDate() {
        return currentDate;
    }

    public void setCurrentDate(Date currentDate) {
        this.currentDate = currentDate;
    }

    ......
}

这里我实验了一下, 从数据库中查询的时候, 生成的SQL语句是:

Hibernate:
    /* SELECT
        m
    from
        Message m */ select
            message0_.id as id1_1_,
            message0_."text" as text2_1_,
            CURRENT_DATE as formula1_
        from
            Message message0_

可见@Formula中的语句会放到外边一个SELECT中的一个列的结果. 不过这里很奇怪, 最后拿到的结果其他字段都正常, 时间还是null, 不知道为什么. 以后再看了. 网上看了一下, @Formula中还必须是原生的SQL语句, 不是直接生成的. 结果只要能返回一个标量值就可以.

列转换器 – @ColumnTransformer

上边还没有实验成功的@Formula, 仅仅只在查询的时候起作用. 列转换器的效果与其有点类似, 但是在读写的时候都会发生作用.

我们现在有一个持久化类, 其中存储着一个重量, 用千克表示, 但是为了精确, 在数据库中存储的是克, 也就是千克*1000, 但是这个类我们对外声称是千克数.

现在我们需要一个自动转换器, 将这个千克类持久化的时候, 在千克对应的字段上自动保存千克*1000之后的结果, 在取出字段写入到Java类的时候, 将数据库中的值除以1000得到千克. 这个类如下:

import javax.persistence.*;

@Entity
public class TestColumnTrans {
    @Id
    @GeneratedValue
    private long id;

    @Column(name = "gram")
    @org.hibernate.annotations.ColumnTransformer(
            read = "gram * 1000",
            write = "? / 1000"
    )
    private long kilo;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public long getKilo() {
        return kilo;
    }

    public void setKilo(long kilo) {
        this.kilo = kilo;
    }

    public TestColumnTrans() {
    }

    @Override
    public String toString() {
        return "TestColumnTrans{" +
                "id=" + id +
                ", kilo=" + kilo +
                '}';
    }
}

首先需要给这个列一个命名方便使用, 然后一看就是原生的Hibernate注解. 这里有两个属性, 分别表示在读出和写入的时候, 实际执行的SQL语句. 在这么设置之后, 无论是查询还是读写,都会与转换之后的值进行比较, 而不是转换之前的值.

写测试如下, 看一下翻译后的SQL语句:

@Test
public void test() {
    EntityManagerFactory emf =
            Persistence.createEntityManagerFactory("HelloWorldPU");
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    tx.begin();


    TestColumnTrans testColumnTrans = new TestColumnTrans();
    testColumnTrans.setKilo(3000000);
    //#1
    em.persist(testColumnTrans);

    tx.commit();

    tx.begin();

    TestColumnTrans testColumnTrans1 = (TestColumnTrans) em.createQuery("select t FROM TestColumnTrans t").getSingleResult();
    #2
    System.out.println(testColumnTrans1);

    #3
    List<TestColumnTrans> testColumnTransList = em.createQuery("select t FROM TestColumnTrans t where t.kilo > 3000000").getResultList();

    System.out.println(testColumnTransList);

    #4
    testColumnTrans1.setKilo(356000);

    tx.commit();
}

#1对应的SQL语句是:

insert
into
    TestColumnTrans
    (gram, id)
values
    (? / 1000, ?)

可以看到按照设置的3000000/1000之后持久化, 实际写入的是3000. #2的语句是:

select
    testcolumn0_.id as id1_2_,
    testcolumn0_.gram * 1000 as gram2_2_
from
    TestColumnTrans testcolumn0_

可以看到选出的时候直接就乘1000. #3语句是:

select
    testcolumn0_.id as id1_2_,
    testcolumn0_.gram * 1000 as gram2_2_
from
    TestColumnTrans testcolumn0_
where
    testcolumn0_.gram * 1000>3000000

从#3语句可以看出, 查询的时候也是用经过转换之后的数值来进行比较, 这个时候就要注意一些了, 可别因为这个kilo属性就用千克去查询, 还是要转换成克的.

#4的update实际上和insert一样, 都是插入转换之后的结果:

update
    TestColumnTrans
set
    gram=? / 1000
where
    id=?

默认值 – 数据库生成/强制默认值/普通默认值

数据库有的时候需要生成一个值, 而不是让外界插入, 比如创建时间, 修改时间, 或者通过触发器安排的一个值.

通常来说, Hibernate在INSERT和UPDATE之后, 都会立刻读取数据, 去获取数据库自动生成的数值, 从而去更新内存中的Java类. 可以使用@org.hibernate.annotations.Generated来让Hibernate而不是数据库来做这个事情:

import org.junit.Test;

import javax.persistence.*;
import java.math.BigDecimal;
import java.util.Date;

@Entity
public class TestGenerated {
    @Id
    @GeneratedValue
    private long id;

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

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public Date getLastModified() {
        return lastModified;
    }

    public void setLastModified(Date lastModified) {
        this.lastModified = lastModified;
    }

    public BigDecimal getInitialPrice() {
        return initialPrice;
    }

    public void setInitialPrice(BigDecimal initialPrice) {
        this.initialPrice = initialPrice;
    }

}

上边的两个属性一个代表值由数据库的触发机制或者其他类似机制生成, 一个代表有默认值, 详细解释如下:

  1. lastModified属性的注解, 表示如何标记一个值由数据库生成的属性, 典型应用就是这种最后修改时间, 一般数据库中使用触发器来生成. @Temporal(TemporalType.TIMESTAMP)表示一个TIMESTAMP字段. 因为java.Util.Date对应的SQL属性有三种时间对象.
    接下来的@Column属性标记了这一列不允许更新, 不允许插入, 意味着这个属性从Hibernate看来是不可变的, 此处这么设置之后, 在Hibernate生成的语句中, 就不包括这一列. 然后@Generated属性中配置了值何时生成. 设置为ALWAYS的时候, Hibernate在每次UPDATE和INSERT之后都会刷新Java对象.
  2. initialPrice属性的注解代表了另外一种情况, 即强制该字段插入之后使用默认值. 对于默认值的情况, Hibernate也认为该字段不能够INSERT, 但能够UPDATE, 所以@Column如此设置. 此外, 还使用了默认值注解ColumnDefault, 注解内容是一个SQL表达式, 也可以使用函数比如”now()”.
    之后@Generated中配置的生成时间是INSERT, 表示只在INSERT之后刷新Java类, 这就表示会获取默认值.

写一个代码来测试一下:

@Test
public void Test() {
    EntityManagerFactory emf =
            Persistence.createEntityManagerFactory("HelloWorldPU");
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    tx.begin();

    TestGenerated tg = new TestGenerated();
    tg.setInitialPrice(new BigDecimal("20"));

//    #1
    em.persist(tg);
    System.out.println(tg);
//    #2
    tg.setInitialPrice(new BigDecimal("40"));

    tx.commit();
}

根据上边的设置, 不允许INSERT, 虽然#1插入的值应该是40, 但此时插入之后, 数据库中仍然使用默认值1.00. 在#2更新之后, 可以看到执行了一条UPDATE语句, 数据库中的数值才变成了40.00

即使将@Column设置为允许INSERT和UPDATE, 插入的时候也依然使用默认值, 因为这个默认值虽然在建表的时候告诉了数据库, 但是如此配置这三个注解之后, 可以看到HQL语句中根本不包含lastModified和initialPrice字段, 因此这两个字段一个固定使用数据库生成值, 一个固定在插入的时候是默认值.

还有第三种控制默认值的方法, 即像默认的数据库操作一样, 那个字段如果插入被允许的其他值, 就采用其他值, 不插入, 就使用默认值, 将initialPrice改写如下:

@Column(nullable = false, columnDefinition = "numeric(8,2) default 1.00")
protected BigDecimal initialPrice = new BigDecimal("1.00");

@Column(nullable = false, columnDefinition = "numeric(8,2) default 1.00")中的红色部分的SQL语句会接在建表的列属性之后, 但是由于不能为null, 所以不能插入没有设置这个字段的新对象. 此时还需要在成员变量上直接标记好对应的默认值.

测试代码如下:

@Test
public void Test() {
    EntityManagerFactory emf =
            Persistence.createEntityManagerFactory("HelloWorldPU");
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    tx.begin();

//  #1
    TestGenerated tg = new TestGenerated();
    TestGenerated tg = new TestGenerated();
    em.persist(tg);

//  #2
    TestGenerated tg1 = new TestGenerated();
    tg1.setInitialPrice(new BigDecimal("40"));
    em.persist(tg1);

    tx.commit();
}

这里#1直接插入一个没有设置initialPrice字段的对象, 可以看到生成的对象initialPrice=1.00. #2插入一个被设置了40.00的tg1对象, 数据库中的值就是40.

时间类型 – @Temporal

刚才已经见过了@Temporal注解, 这个注解时间上就是精确指定具体是哪种时间类型.

Hibernate支持java.Util.Date, java.Util.Calendar, java.sql.Date, java.sql.Time, java.sql.Timestamp 以及 JDK 8的java.time库.

但是Date, Calendar等类的时间大二全, Hibernate对于这些属性, 默认使用最全的TIMESTAMP类型. 如果想做精确的指定, 要么使用java.sql.*的类型, 要么就使用@Temporal注解来指定具体的类型, 这样时间就可以自动转换.

此外还有一个有用的注解经常用于创建生成时间:

@Temporal(TemporalType.TIMESTAMP)
@Column(updatable = false)
@org.hibernate.annotations.CreationTimestamp
protected Date createdOn;

搭配只允许INSERT的时候使用, 就可以自动创建一条记录的创建时间, 实在非常好用. 所以很多时候通过@Column将读写控制交给框架, 而不是数据库, 确实更加方便. 与其类似的, 还有一个@UpdateTimestamp注解, 用于自动更新每次的时间. 这里不禁想起了Django ORM每次控制这种字段的方式.

枚举类型 – @Enumerated

还有一个比较特殊的是枚举类型, 枚举类实际上不是被当成一个类来使用, 而是被当成一系列值常量值来使用. 使用@Enumerated来标注一个枚举类型, 有如下的枚举类:

public enum AnimalType {
    KIWI,OWL,PENGUIN
}

然后就是使用这个枚举类的持久化类:

import org.junit.Test;
import javax.persistence.*;

@Entity
public class TestEnum {

    @Id
    @GeneratedValue
    private long id;

    @Enumerated(EnumType.STRING)
    private AnimalType animalType;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public AnimalType getAnimalType() {
        return animalType;
    }

    public void setAnimalType(AnimalType animalType) {
        this.animalType = animalType;
    }

    @Test
    public void test() {
        EntityManagerFactory emf =
                Persistence.createEntityManagerFactory("HelloWorldPU");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        TestEnum testEnum = new TestEnum();
        TestEnum testEnum1 = new TestEnum();


        testEnum.setAnimalType(AnimalType.KIWI);
        testEnum1.setAnimalType(AnimalType.OWL);

        em.persist(testEnum);
        em.persist(testEnum1);

        tx.commit();
    }
}

@Enumerated(EnumType.STRING)中间的string表示存储枚举的时候, 存储枚举字符串. 测试中生成表的HQL语句将枚举类型生成为一个varchar字段.

还一个设置是EnumType.ORDINAL, 表示用枚举类型对应的整数来存储. 一般来说, 使用字符串的可读性会高一些, 但是@Enumerated()的默认是ORDINAL.

最后遗留了一个问题, 就是@Formula虽然没有报错, 但是也没有获取到值, 看来还得继续学学.