红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Spring Boot 学习笔记
  4. 正文

JPA 学习笔记 1:开始

2025年10月3日 137点热度 0人点赞 0条评论

快速开始

JPA 是 Sun 对 ORM 框架定义的一组标准规范,类似于 JDBC 之于各种数据库。

创建一个空的 Maven 项目,添加如下依赖:

<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>7.1.2.Final</version>
</dependency>
<dependency>
    <groupId>jakarta.annotation</groupId>
    <artifactId>jakarta.annotation-api</artifactId>
    <version>3.0.0</version>
</dependency>
<dependency>
    <groupId>jakarta.persistence</groupId>
    <artifactId>jakarta.persistence-api</artifactId>
    <version>3.2.0</version>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>9.4.0</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.42</version>
</dependency>

添加 JPA 的配置文件src/main/resources/META-INF/persistence.xml:

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
             version="2.0">
    <persistence-unit name="jpa-demo" transaction-type="RESOURCE_LOCAL">
        <!-- 指定ORM框架实现了 PersistenceProvider 接口的实现类 -->
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <class>cn.icexmoon.entity.Customer</class>
        <properties>
            <!-- 数据库连接设置 -->
            <property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver" />
            <property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/jpa" />
            <property name="jakarta.persistence.jdbc.user" value="root" />
            <property name="jakarta.persistence.jdbc.password" value="mysql" />
            <!-- ORM 框架设置 -->
            <property name="jakarta.persistence.schema-generation.database.action" value="update"/>
            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.format_sql" value="true" />
            <property name="hibernate.highlight.sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

JPA 配置文件的格式和内容与 JPA 以及具体的 JPA 实现版本有关,不同的版本可能会有不同的差异,这里是 Hibernate 7.x 的配置内容。

这里的<class>cn.icexmoon.entity.Customer</class>是之后定义的实体类,如果没有在这里添加,就会报错表示实体类不属于这个persistence-unit。

根据需要,JPA 会在 SessionFactory 启动以及关闭时自动执行一些 DDL 语句对数据库进行初始化,具体的策略由配置项 jakarta.persistence.schema-generation.database.action决定,具体有以下选项:

  • drop-and-create,先删除数据库,然后创建表、序列和索引,最后填充初始数据。

  • create,创建表、序列和约束,填充初始数据。

  • create-drop,在启动时先删除再创建数据库,在关闭时删除数据库。

  • drop,关闭时删除数据库。

  • validate,仅检查数据库是否与 JPA 中的定义匹配,不进行任何更改。

  • update,仅在数据库与 JPA 中的定义不匹配时执行 DDL 语句更新数据库。

  • populate,仅填充初始数据,不执行任何 DDL 语句。

添加实体类src/main/java/cn/icexmoon/entity/Customer.java:

@Entity
@Table(name = "customer")
@Data
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "last_name", length = 10, nullable = false)
    private String lastName;
    private Integer age;
}

主要涉及以下注解:

  • @Entity,定义 JPA 实体

  • @Table,如果实体名称与表名不同,需要使用 @Table 标记对应的数据库表

  • @Id,标记用于表主键的字段

  • @GeneratedValue,主键生成策略,分为以下几种:

    • AUTO:JPA 自动选择合适策略,默认选项

    • IDENTITY:采用数据库自增方式生成主键,Oracle 不支持

    • SEQUENCE:通过序列产生主键,通过 @SequenceGenerator 注解指定序列名,MySQL 不支持

    • TABLE:通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库迁移

  • @Column,定义列的相关设置,比如列名、长度、是否为空、是否唯一约束等。

添加入口类src/main/java/cn/icexmoon/Main.java:

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello world!");
        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpa-demo");
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        EntityTransaction transaction = entityManager.getTransaction();
        transaction.begin();
        Customer customer = new Customer();
        customer.setLastName("icexmoon");
        customer.setAge(18);
        entityManager.persist(customer);
        transaction.commit();
        entityManager.close();
        entityManagerFactory.close();
    }
}

注解

@Transient

JPA 实体中默认所有具备 Getter 和 Setter 的属性都是表字段的映射,如果有属性不是表字段的映射,就需要使用@Transient注解标记:

@Entity
@Table(name = "customer")
@Data
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "last_name", length = 10, nullable = false)
    private String lastName;
    private Integer age;
    @Transient
    private Integer score;
}

这里的score属性就不会作为表字段保存和读取。

@Temporal

如果实体类的字段是Date类型:

@Entity
@Table(name = "customer")
@Data
public class Customer {
    // ...
    private Date birth;
    private Date createTime;
}

JPA 自动生成的数据库字段都会是datetime类型。在这里,birth字段使用date类型的数据库字段存储更为合适。

可以使用@Temporal注解显式为 Date 类型字段指定数据库字段的类型:

@Entity
@Table(name = "customer")
@Data
public class Customer {
    // ...
    @Temporal(TemporalType.DATE)
    private Date birth;
    private Date createTime;
}

此时数据库中的birth字段会使用date类型而非datetime类型。

在高版本的 JPA 中@Temporal虽然依然有效,但已经被标记为废弃,高版本的 JPA 中建议直接使用 LocalDate和LocalDateTime定义时间类型的实体字段。

ID 属性

实体类都必须有用@Id标记的 ID 属性。

或者从父类(根实体)继承。

生成策略

需要为 ID 属性指定生成策略,Hibernate 支持以下 ID 属性生成策略:

Strategy 策略 Java type Java 类型 Implementation 实现
GenerationType.UUID UUID 或 String 一个 Java UUID
GenerationType.IDENTITY Long 或 Integer 一个标识符或自增列
GenerationType.SEQUENCE Long 或 Integer 数据库序列
GenerationType.TABLE Long 或 Integer 一个数据库表
GenerationType.AUTO Long 或 Integer 根据数据库的标识符类型和功能选择 SEQUENCE 、 TABLE 或 UUID

示例:

@Entity
@Data
@NoArgsConstructor
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;
    // ...
}

此时数据库表主键将使用 Java 生成的 UUID,数据库存储的类型是binary(16)。

@Entity
@Data
@NoArgsConstructor
@Table(name = "person")
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    // ...
}

此时数据库表主键将使用数据库表的自增序列,即id bigint auto_increment。

@Entity
@Data
@NoArgsConstructor
@Table(name = "person")
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
    // ...
}

此时将使用额外的表(person_seq)用于存储和生成自增序列。

@Entity
@Data
@NoArgsConstructor
@Table(name = "person")
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.TABLE)
    private Long id;
    // ...
}

使用额外的表(hibernate_sequences) 生成主键。

@Entity
@Data
@NoArgsConstructor
@Table(name = "person")
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    // ...
}

由 Hiberrnate 自动决定使用何种生成策略,对于 MySQL 8,使用的是 GenerationType.TABLE。

对于不同的数据库,同一个主键生成策略可能有不同的实现方式,上面讨论的都是基于 MySQL 8 的主键策略实现。

使用表生成主键

使用表生成主键的策略时,默认使用统一的表hibernate_sequences,可以自定义使用的表以及自增序列查找方式。

创建用于生成主键的表:

create table id_generator
(
    id         bigint unsigned auto_increment
    primary key,
    table_name varchar(255) not null,
    value      bigint       null
);

修改实体类,使用表生成主键:

@Entity
@Table(name = "customer")
@Data
public class Customer {
    @Id
    @TableGenerator(
            name = "customer_id_generator",
            table = "id_generator",
            pkColumnName = "table_name",
            valueColumnName = "value",
            pkColumnValue = "customer",
            allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "customer_id_generator")
    private Long id;
    // ...
}

@TableGenerator的table属性说明使用id_generator表生成主键,生成主键的数据通过pkColumnName、pkColumnValue、valueColumnName三个属性进行定位,即pkColumnName的值所在的列的值为pkColumnValue的值,游标存放在valueColumnName值对应的列。allocationSize属性决定了下一个生成的主键值的步进。

当@TableGenerator与@GeneratedValue一起使用时,@GeneratedValue的属性是可以省略的:

@Id
@TableGenerator(
    name = "customer_id_generator",
    table = "id_generator",
    pkColumnName = "table_name",
    valueColumnName = "value",
    pkColumnValue = "customer",
    allocationSize = 1)
@GeneratedValue
private Long id;

联合主键

并不是所有表都有唯一列作为主键,有时候有多个列(一般称作复合主键或联合主键),对应到 JPA 实体,需要使用多个属性作为联合主键。

比如有三张表:学生表、课程表、学生选课表。

对应的实体:

@Data
@Entity
@NoArgsConstructor
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(length = 10, nullable = false, unique = true)
    private String name;
    @Column(columnDefinition = "tinyint unsigned")
    private Integer age;
​
    public Student(String name) {
        this.name = name;
    }
}
@Entity
@Data
@NoArgsConstructor
public class ClassItem {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(length = 10, nullable = false)
    private String name;
    @Column(length = 100)
    private String description;
​
    public ClassItem(String name) {
        this.name = name;
    }
}
@Entity
@Data
@IdClass(StudentClass.StudentClassId.class)
@NoArgsConstructor
public class StudentClass {
    public record StudentClassId(Long classId, Long studentId) {
    }
​
    @Id
    private Long classId;
    @Id
    private Long studentId;
    @Temporal(TemporalType.DATE)
    private Date beginDate;
    @Temporal(TemporalType.DATE)
    private Date endDate;
    public StudentClass(StudentClassId id, Date beginDate, Date endDate) {
        this.classId = id.classId;
        this.studentId = id.studentId;
        this.beginDate = beginDate;
        this.endDate = endDate;
    }
}

学生选课表中有两个属性作为联合主键,它们都有@Id注解。为了能使用find等 API 通过主键进行检索,这里还需要额外定义一个包含了这两个属性的类StudentClassId,并使用@IdClass注解进行标记。

利用联合主键添加数据:

Student student = new Student("李四");
entityManager.persist(student);
ClassItem classItem = new ClassItem("java");
entityManager.persist(classItem);
StudentClass studentClass = new StudentClass(
    new StudentClass.StudentClassId(classItem.getId(), student.getId()),
    DateUtil.parseDate("2025-1-25"), DateUtil.parseDate("2025-10-25"));
entityManager.persist(studentClass);

查找数据:

StudentClass studentClass = entityManager.find(StudentClass.class, new StudentClass.StudentClassId(1L, 1L));
System.out.println(studentClass);

虽然像上面那样定义联合主键是可行的,但并不推荐,Hibernate 官方推荐的方式是:

@Entity
@Data
@NoArgsConstructor
public class StudentClass {
    public record StudentClassId(Long classId, Long studentId) {
    }
    @EmbeddedId
    private StudentClassId id;
​
    @Temporal(TemporalType.DATE)
    private Date beginDate;
    @Temporal(TemporalType.DATE)
    private Date endDate;
    public StudentClass(StudentClassId id, Date beginDate, Date endDate) {
        this.beginDate = beginDate;
        this.endDate = endDate;
    }
}

这里使用@EmbeddedId定义了一个联合主键,不需要使用@IdClass以及多个@Id注解。

自然键

实体除了可以用 ID 属性唯一定位,还可能通过其他属性组合唯一定位,这些属性被称作自然键(Nacture Key)。

@Entity
@Data
@NoArgsConstructor
public class Car {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String brand;
    @NaturalId
    private String engineCode;
​
    public Car(String brand, String engineCode) {
        this.brand = brand;
        this.engineCode = engineCode;
    }
}

上面的示例中,除了可以用 ID 唯一确定一辆车,还可以通过发动机序列号唯一确定一辆车。

为一个(或多个)属性添加@NaturalId注解后,Hibernate 会为这些属性对应的列创建单列(或联合)唯一索引。

@Basic

ID 属性意外的持久化属性可以用@Basic注解标注:

@Entity
@Data
@NoArgsConstructor
public class Car {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Basic
    private String brand;
    @NaturalId
    @Basic
    private String engineCode;
    // ...
}

但通常@Basic注解会被省略,这些属性会被默认为实体的持久化属性:

@Entity
@Data
@NoArgsConstructor
public class Car {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String brand;
    @NaturalId
    private String engineCode;
    // ...
}

@Basic的最常见用途是标注一个属性是非 Null 的:

@NaturalId
@Basic(optional = false)
private String engineCode;

此时执行测试:

Car car = new Car();
car.setBrand("奔驰");
entityManager.persist(car);

会报错,Hibernate 会在执行 SQL 前检查相应的属性是否为 NULL。

当然也可以使用@Column(nullable = false),但它们是有区别的,@Column仅表示实体映射到数据库中的结构,不约束实体本身,换言之这样设置后如果相应字段为 NULL,报错会发生在数据库 SQL 执行时。

如果要定义非 NULL 实体属性,一个更好的做法是使用 Hibernate Validator 的@NotNull注解。

需要添加 Hibernate Validator 依赖:

<!-- Jakarta Expression Language (EL) -->
<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>jakarta.el</artifactId>
    <version>4.0.2</version>
</dependency>
<!-- 确保 Hibernate Validator 版本兼容 -->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>8.0.1.Final</version>
</dependency>

使用@NotNull注解标注非Null持久属性:

@NaturalId
@NotNull
private String engineCode;

Version 属性

可以通过 Version 属性为实体开启乐观锁。

先看一个示例:

测试用的实体类:

@Entity
@Data
@NoArgsConstructor
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(length = 10, nullable = false)
    private String name;
    @Column(columnDefinition = "decimal(10,2)")
    private BigDecimal price;
​
    public Book(String name, BigDecimal price) {
        this.name = name;
        this.price = price;
    }
}

测试用例:

// 先准备一些数据
Book book = new Book("MySQL 应知应会", new BigDecimal("10.0"));
entityManager.persist(book);
transaction.commit();
transaction.begin();
Book book1 = entityManager.find(Book.class, book.getId());
book1.setPrice(new BigDecimal("20.0"));
// 模拟在某个地方被其他线程修改了
CountDownLatch  countDownLatch = new CountDownLatch(1);
new Thread(() -> {
    transaction.commit();
    entityManager = entityManagerFactory.createEntityManager();
    transaction = entityManager.getTransaction();
    transaction.begin();
    Book book3 = entityManager.find(Book.class, book.getId());
    book3.setPrice(new BigDecimal("40.0"));
    entityManager.merge(book3);
    transaction.commit();
    transaction.begin();
    System.out.println("另外一个线程修改了数据");
    countDownLatch.countDown();
}).start();
countDownLatch.await();
entityManager.merge(book1);
System.out.println("当前线程修改了数据");

当前线程读取数据后,修改了某些属性,在提交修改到数据库前,其它线程读取并修改了该行数据。此时当前线程再提交修改就会导致其它线程的修改丢失了。

这种问题本质上是并发时修改相同的共享资源,可以通过加锁(或者分布式锁)来解决,JPA 也提供一个 Version 属性,可以简单地在表上实现一个乐观锁。

@Entity
@Data
@NoArgsConstructor
public class Book {
    // ...
    @Version
    @Column(columnDefinition = "int unsigned")
    private Integer version;
    // ...
}

这里的 version 字段初始默认是0,每次执行 UPDATE 语句后会自增。此时再通过 JPA 更新数据时会检查 version,比如:

Hibernate: 
    update
        Book 
    set
        name=?,
        price=?,
        version=? 
    where
        id=? 
        and version=?

如果 version 与读取时的 version 不匹配,就会报错:

jakarta.persistence.OptimisticLockException: Row was already updated or deleted by another transaction for entity [cn.icexmoon.entity.Book with id '13']

Hibernate 官方建议对经常修改的实体都应该设置 version 属性。

Version 属性除了可以使用常见的 int 类型,还可以使用时间等类型,具体可以参考官方文档。

枚举类型

JPA 使用 @Enumerated 注解标记枚举类型的实体属性:

@Entity
@Data
@NoArgsConstructor
public class Car {
    // ...
    @Enumerated
    private Color color;
    // ...
}

默认这些属性将以枚举实例的ordinal()方法的值(定义的顺序)存储,可以改变为使用枚举实例的名称存储:

@Enumerated(EnumType.STRING)
private Color color;

除此之外,也可以使用枚举实例中自定义的属性值进行存储:

public enum Color{
    Yellow(1),Red(2),Blue(3);
    @EnumeratedValue
    final int value;
    Color(int value){
        this.value = value;
    }
}

实体类继承

实体类之间可以继承,没有继承任意实体类的实体类被称作根实体(Root Entity)。根实体必须有 Id 属性,且可以是普通类或抽象类。

比如可以将每个表都有的通用字段定义在根实体中:

@Data
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class RootEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.TABLE)
    private Long id;
    private Date createTime;
    @Column(length = 10)
    private String createUser;
    private Date updateTime;
    @Column(length = 10)
    private String updateUser;
}

根实体必须定义 ID 属性,子实体:

@Table(name = "child")
@Entity
@Data
@NoArgsConstructor
public class ChildEntity extends RootEntity {
    private String name;
    public ChildEntity(String name) {
        this.name = name;
    }
}

子实体中不能有 ID 属性。这种实体互相继承存在一些限制,比如如果子实体的表拥有全部独立字段,就需要在根实体中使用@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS),且此时的 ID 属性生成策略不能是@GeneratedValue(strategy = GenerationType.IDENTITY)。

更推荐的方式是将通用字段定义在一个非实体的普通类中:

@Data
@MappedSuperclass
public abstract class BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Date createTime;
    @Column(length = 10)
    private String createUser;
    private Date updateTime;
    @Column(length = 10)
    private String updateUser;
}

@MappedSuperclass注解表明子实体类可以继承基类的映射关系(包括主键)。

实体类直接继承这个普通类:

@Table(name = "school")
@Entity
@Getter
@Setter
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@NoArgsConstructor
public class School extends BaseEntity{
    private String name;
​
    public School(String name) {
        this.name = name;
    }
}

继承策略

有三种不同的继承策略:

  • SINGLE_TABLE,所有继承层次上的实体都映射到同一张表上保存,用某个字段进行区分

  • JOINED,将继承层次上的实体映射到单独的表,但每张表只保存实体声明的属性(不包含继承的属性)

  • TABLE_PER_CLASS,将继承层次上的实体映射到单独的表,每张表包含全部的属性

使用SINGLE_TABLE策略继承实体的示例:

@Entity
@Data
@NoArgsConstructor
@Table(name = "person_v2")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(discriminatorType = DiscriminatorType.CHAR, name = "kind")
@DiscriminatorValue("P")
public class Person2 {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
​
    @Column(length = 20)
    private String name;
    private Integer age;
}
@Entity
@Data
@NoArgsConstructor
@DiscriminatorValue("A")
public class Author2 extends Person2 {
    private String penName;
}

在根实体上,使用@Inheritance定义继承策略,在这里是可选的,因为默认的继承策略就是SINGLE_TABLE。

当继承策略是SINGLE_TABLE时,必须使用@DiscriminatorColumn注解指定同一张表中区分不同实体数据的字段类型和字段名称。@DiscriminatorValue注解指定所在实体在区分列中的值。

子实体中只需要使用@DiscriminatorValue注解,因为是同一张表,子实体不需要定义@Id属性,以及不能使用@Table。

最终生成的表和测试数据:

image-20251031090713682

使用JOINED继承策略的示例:

@Entity
@Data
@NoArgsConstructor
@Table(name = "person_v3")
@Inheritance(strategy = InheritanceType.JOINED)
public class Person3 {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
​
    @Column(length = 20)
    private String name;
    private Integer age;
}
@Entity
@Data
@NoArgsConstructor
@Table(name = "author_v3")
public class Author3 extends Person3{
    private String penName;
}

根实体生成的表person_v3:

image-20251031091709483

子实体生成的表author_v3:

image-20251031091722393

TABLE_PER_CLASS的继承策略示例之前已经说过了,这里不再赘述。这种方式不推荐,应当考虑继承普通类并使用@MappedSuperclass注解。

自定义类型转换

可以自定义类型转换器以处理更多 Hibernate 不支持的实体属性类型:

@Converter(autoApply = true)
public class EnumSetConverter
        // converts Java values of type EnumSet<DayOfWeek> to integers for storage in an INT column
        implements AttributeConverter<EnumSet<DayOfWeek>,Integer> {
    @Override
    public Integer convertToDatabaseColumn(EnumSet<DayOfWeek> enumSet) {
        int encoded = 0;
        var values = DayOfWeek.values();
        for (int i = 0; i<values.length; i++) {
            if (enumSet.contains(values[i])) {
                encoded |= 1<<i;
            }
        }
        return encoded;
    }

    @Override
    public EnumSet<DayOfWeek> convertToEntityAttribute(Integer encoded) {
        var set = EnumSet.noneOf(DayOfWeek.class);
        var values = DayOfWeek.values();
        for (int i = 0; i<values.length; i++) {
            if (((1<<i) & encoded) != 0) {
                set.add(values[i]);
            }
        }
        return set;
    }
}

因为类型转换器类使用了@Converter(autoApply = true)注解,因此只要这个类注册到 JPA 中,任何实体类中的EnumSet<DayOfWeek>类型的持久属性都会使用这个类型转换器。

@Entity
@Data
public class Week {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private EnumSet<DayOfWeek> workDays;
    private EnumSet<DayOfWeek> holidays;
}

也可以不使用@Converter(autoApply = true),转而在实体属性上指定使用的类型转换器:

@Convert(converter = EnumSetConverter.class)
private EnumSet<DayOfWeek> workDays;
@Convert(converter = EnumSetConverter.class)
private EnumSet<DayOfWeek> holidays;

JSON

将自定义类型以 JSON 的形式存储和读取是很常见的需求,在 Hibernate 中可以简单通过以下方式实现:

@Entity
@Data
@NoArgsConstructor
@Table(name = "person")
public class Person {
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Name{
        private String firstName;
        private String lastName;
    }
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @JdbcTypeCode(SqlTypes.JSON)
    private Name name;
	// ...
}

这里对自定义类型的实体属性name使用了@JdbcTypeCode(SqlTypes.JSON)注解,这样做可以显式指定 Hibernate 使用特定的表字段类型映射实体属性。

将一个实体属性映射到 JSON 类型的表字段上时,Hibernate 会自动调用类路径上可用的 JSON 转换中间件(通常时 jackson)进行 JSON 的序列化/反序列化。因此如果没有相关中间件,需要添加:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>${jackson.version}</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>${jackson.version}</version>
</dependency>

实际测试发现 hibernate 7.1.x 不能使用最新的 jackson 依赖(>=3.0),否则会报错。

嵌入类型

如果表中某些字段可以映射到一个类型进行组织,可以将其定义为嵌入类型(Embeddable Type),比如:

@Entity
@Data
@NoArgsConstructor
@Table(name = "person")
public class Person {
    @Embeddable
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Name{
        private String firstName;
        private String lastName;
    }
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Name name;
    // ...
}

name 属性将映射到表中的多个字段进行存储:

create table person
(
    id        bigint auto_increment
        primary key,
    age       int          null,
    firstName varchar(255) null,
    lastName  varchar(255) null
);

这里可以使用record替代静态内部类,进一步简化代码:

@Entity
@Data
@NoArgsConstructor
@Table(name = "person")
public class Person {
    @Embeddable
    public record Name(String firstName, String lastName) {
    }
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Name name;
	// ...
}

JSON

如果不想用多个字段保存嵌入类型,也可以 JSON 序列化后用一个字段保存:

@Entity
@Data
@NoArgsConstructor
@Table(name = "person")
public class Person {
    @Embeddable
    public record Name(String firstName, String lastName) {
    }
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @JdbcTypeCode(SqlTypes.JSON)
    private Name name;
	// ...
}

嵌入类型可以嵌套,即一个嵌入类型中包含其它嵌入类型的属性。嵌入类型不能单独存在,它们依附于实体。

数组

Hibernate(非 JPA)可以将实体中的数组类型的属性存储为表中的一列:

@Entity
@Data
public class Week2 {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Array(length = 7)
    private DayOfWeek[] workDays;
    @Array(length = 2)
    private DayOfWeek[] holidays;
}

数组元素的类型可以是基本类型或者枚举,映射的目标列类型取决于数据库对 Array 类型的支持,在 MySQL 会使用 JSON 类型的列存储。

ElementCollection

JPA 有一个@ElementCollection注解,可以将标注的基本类型组成的容器属性用额外的表进行保存,并通过外键关联到实体表:

@Entity
@Data
public class Week3 {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @ElementCollection
    private List<DayOfWeek> workDays;
    @ElementCollection
    private List<DayOfWeek> holidays;
}

不过这种方式局限性很大,不推荐使用。

监听器

可以对实体类应用监听器,以处理实体类不同生命周期的特殊需要:

@Data
@MappedSuperclass
@EntityListeners(BaseEntity.BaseEntityEvents.class)
public abstract class BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Date createTime;
    @Column(length = 10)
    private String createUser;
    private Date updateTime;
    @Column(length = 10)
    private String updateUser;
    public static class BaseEntityEvents {
        @PrePersist
        public void prePersist(BaseEntity baseEntity) {
            baseEntity.setCreateTime(new Date());
            baseEntity.setUpdateTime(new Date());
        }

        @PreUpdate
        public void preUpdate(BaseEntity baseEntity) {
            baseEntity.setUpdateTime(new Date());
        }
    }
}

这里在实体类的基类上创建了一个监听器类(BaseEntityEvents),并使用@EntityListeners(BaseEntity.BaseEntityEvents.class)注解绑定到实体类上,这样继承了基类的实体类在更新和创建时会自动添加创建时间和更新时间。

监听中可以从容器中依赖注入(如果使用了容器框架),因此可以实现更广泛的用途,比如获取当前登录用户信息,在更新或创建时将更新人/创建人的信息也写入数据库通用字段。

本文的完整示例代码可以从这里获取。

参考资料

  • 尚硅谷jpa开发教程全套完整版

  • 集成JPA - Java教程 - 廖雪峰的官方网站

  • 《Hibernate 7 短指南》 --- A Short Guide to Hibernate 7

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: jpa
最后更新:2025年10月31日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号