快速开始
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:
(name = "customer")
public class Customer {
(strategy = GenerationType.IDENTITY)
private Long id;
(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注解标记:
(name = "customer")
public class Customer {
(strategy = GenerationType.IDENTITY)
private Long id;
(name = "last_name", length = 10, nullable = false)
private String lastName;
private Integer age;
private Integer score;
}
这里的score属性就不会作为表字段保存和读取。
@Temporal
如果实体类的字段是Date类型:
(name = "customer")
public class Customer {
// ...
private Date birth;
private Date createTime;
}
JPA 自动生成的数据库字段都会是datetime类型。在这里,birth字段使用date类型的数据库字段存储更为合适。
可以使用@Temporal注解显式为 Date 类型字段指定数据库字段的类型:
(name = "customer")
public class Customer {
// ...
(TemporalType.DATE)
private Date birth;
private Date createTime;
}
此时数据库中的birth字段会使用date类型而非datetime类型。
在高版本的 JPA 中
@Temporal虽然依然有效,但已经被标记为废弃,高版本的 JPA 中建议直接使用LocalDate和LocalDateTime定义时间类型的实体字段。
使用表生成主键
创建用于生成主键的表:
create table id_generator
(
id bigint unsigned auto_increment
primary key,
table_name varchar(255) not null,
value bigint null
);
修改实体类,使用表生成主键:
(name = "customer")
public class Customer {
(
name = "customer_id_generator",
table = "id_generator",
pkColumnName = "table_name",
valueColumnName = "value",
pkColumnValue = "customer",
allocationSize = 1)
(strategy = GenerationType.TABLE, generator = "customer_id_generator")
private Long id;
// ...
}
@TableGenerator的table属性说明使用id_generator表生成主键,生成主键的数据通过pkColumnName、pkColumnValue、valueColumnName三个属性进行定位,即pkColumnName的值所在的列的值为pkColumnValue的值,游标存放在valueColumnName值对应的列。allocationSize属性决定了下一个生成的主键值的步进。
本文的完整示例代码可以从获取。
联合主键
并不是所有表都有唯一列作为主键,有时候有多个列(一般称作复合主键或联合主键),对应到 JPA 实体,需要使用多个属性作为联合主键。
比如有三张表:学生表、课程表、学生选课表。
对应的实体:
public class Student {
(strategy = GenerationType.IDENTITY)
private Long id;
(length = 10, nullable = false, unique = true)
private String name;
(columnDefinition = "tinyint unsigned")
private Integer age;
public Student(String name) {
this.name = name;
}
}
public class ClassItem {
(strategy = GenerationType.IDENTITY)
private Long id;
(length = 10, nullable = false)
private String name;
(length = 100)
private String description;
public ClassItem(String name) {
this.name = name;
}
}
(StudentClass.StudentClassId.class)
public class StudentClass {
public record StudentClassId(Long classId, Long studentId) {
}
private Long classId;
private Long studentId;
(TemporalType.DATE)
private Date beginDate;
(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 官方推荐的方式是:
public class StudentClass {
public record StudentClassId(Long classId, Long studentId) {
}
private StudentClassId id;
(TemporalType.DATE)
private Date beginDate;
(TemporalType.DATE)
private Date endDate;
public StudentClass(StudentClassId id, Date beginDate, Date endDate) {
this.beginDate = beginDate;
this.endDate = endDate;
}
}
这里使用@EmbeddedId定义了一个联合主键,不需要使用@IdClass以及多个@Id注解。
Version 属性
可以通过 Version 属性为实体开启乐观锁。
先看一个示例:
测试用的实体类:
public class Book {
(strategy = GenerationType.IDENTITY)
private Long id;
(length = 10, nullable = false)
private String name;
(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 属性,可以简单地在表上实现一个乐观锁。
public class Book {
// ...
(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 类型,还可以使用时间等类型,具体可以参考。
实体类继承
实体类之间可以继承,因此可以将一些所有表共有的公共字段定义在基类中:
public abstract class BaseEntity {
(strategy = GenerationType.IDENTITY)
private Long id;
private Date createTime;
(length = 10)
private String createUser;
private Date updateTime;
(length = 10)
private String updateUser;
}
@MappedSuperclass注解表明子实体类可以继承基类的映射关系(包括主键)。
子实体类:
(name = "school")
(callSuper = true)
(callSuper = true)
public class School extends BaseEntity{
private String name;
public School(String name) {
this.name = name;
}
}
监听器
可以对实体类应用监听器,以处理实体类不同生命周期的特殊需要:
(BaseEntity.BaseEntityEvents.class)
public abstract class BaseEntity {
(strategy = GenerationType.IDENTITY)
private Long id;
private Date createTime;
(length = 10)
private String createUser;
private Date updateTime;
(length = 10)
private String updateUser;
public static class BaseEntityEvents {
public void prePersist(BaseEntity baseEntity) {
baseEntity.setCreateTime(new Date());
baseEntity.setUpdateTime(new Date());
}
public void preUpdate(BaseEntity baseEntity) {
baseEntity.setUpdateTime(new Date());
}
}
}
这里在实体类的基类上创建了一个监听器类(BaseEntityEvents),并使用@EntityListeners(BaseEntity.BaseEntityEvents.class)注解绑定到实体类上,这样继承了基类的实体类在更新和创建时会自动添加创建时间和更新时间。
监听中可以从容器中依赖注入(如果使用了容器框架),因此可以实现更广泛的用途,比如获取当前登录用户信息,在更新或创建时将更新人/创建人的信息也写入数据库通用字段。
本文的完整示例代码可以从
参考资料

文章评论