从零开始 Spring Boot 48:JPA & Hibernate
图源:简书 (jianshu.com)
对象关系映射(ORM)是将Java对象转换为数据库表的过程。换句话说,这允许我们在没有任何SQL的情况下与关系数据库进行交互。Java Persistence API(JPA)是一个定义如何在Java应用程序中持久化数据的规范。JPA的主要焦点是ORM层。
Hibernate是目前使用的最流行的Java ORM框架之一。它的第一个版本几乎是20年前的事了,现在仍然有优秀的社区支持和定期发布。此外,Hibernate是JPA规范的标准实现,它还具有一些特定于Hibernate的附加特性。
以上内容摘抄自Learn JPA & Hibernate | Baeldung。
本文将简单介绍如何在 Spring Boot 中使用 JPA 和 Hibernate。
当然,这是一个相当宏大的议题,所以本篇文章只做一个入门介绍和引导。
准备
要使用 JPA 和 Hibernate,需要添加spring-boot-starter-data-jpa
依赖,此外还需要添加你所使用的数据库驱动,我这里使用的是Mysql
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
spring-boot-starter-data-jpa
中包含 JDBC 相关依赖,所以不用手动添加 JDBC 相关依赖。
自然的,你还需要添加数据库相关配置:
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=mysql
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.database=MYSQL
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
比较特别的是,这里还添加了 JPA 的相关配置(spring.jpa.xxx
),这里使了一下配置:
spring.jpa.database
,要操作的目标数据库(类型),默认情况下自动检测。也可以使用spring.jpa.database-platform
属性进行设置。spring.jpa.hibernate.ddl-auto
,DDL模式。这实际上是hibernate.hbm2ddl.auto
属性的快捷方式。当使用嵌入式数据库并且未检测到架构管理器时,默认为“create-drop”。否则,默认为“无”。spring.jpa.show-sql
,是否启用SQL语句的日志记录(在控制台打印相关SQL)。
使用 Hibernate 的 DDL 模式有以下几种:
- vlidate,每次加载 Hibernate 时,验证数据库表结构,将表结构与本地 model 进行对比,但不会创建新表,也不会插入数据。
- create,每次加载 Hibernate 时,删除本地 model 对应的表,并使用本地 model 重新生成表结构。
- create-drop,加载 Hibernate 时根据本地 model 生成表结构,SessionFactory 关闭后删除生成的表结构。
- update,加载 Hibernate 时,如果数据库中缺少 model 对应的表结构,创建,否则将对比表结构和 model,如果不同,将使用 update DDL 对表结构进行更新。
一般而言,对于持久型数据库(如MySQL),使用update
模式,对于内存数据库(如H2),使用create-drop
模式。
Entity
每一个数据库表,对应到一个实体类(Entity):
@Entity
public class Student {
}
这个实体类用@Entity
注解标识,默认情况下实体名称为类名。
如果表名与实体名称不同,需要使用@Table
注解:
@Entity
@Table(name = "USER_STUDENT")
public class Student {
// ...
}
虽然这里使用的表名是大写(USER_STUDENT
),但实际上 Hibernate 会将其转化为全小写(user_student
)后用于数据库查询或 DDL 语句。
必须要为实体类指定一个主键以对应数据库表的主键:
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
// ...
}
@Id
表名字段是实体类的主键,@GeneratedValue
指明主键的生成方式:
AUTO
,持久层为特定数据库使用适当的策略生成主键。UUID
,持久层生成 RFC 4122 通用唯一标识作为主键。IDENTITY
,使用数据库标识列(自增主键)作为主键。SEQUENCE
,使用数据库序列(xxx_seq表)作为主键。TABLE
,使用基本数据库表生成唯一主键。
对于普通列对应的字段,使用@Column
注解:
public class Student {
// ...
@Column(name = "NAME", length = 50, nullable = false, unique = false)
private String name;
@Column(name = "BIRTH_DAY", nullable = false)
private LocalDate birthDay;
// ...
}
比较特别的是,如果字段类型是旧的时间类型(比如
java.util.Date
),就需要使用@Temporal
注解进行转换,详细可以阅读这篇文章Hibernate – Mapping Date and Time | Baeldung。
如果某个字段不需要映射到数据库表:
public class Student {
// ...
@Transient
private Integer age;
}
序列化和反序列化时要排除的字段使用
transient
关键字声明,和这里的@Transient
注解有着类似的作用和命名方式。
对于枚举字段,可以指定其用字面量存储还是顺序值:
public class Student {
// ...
@Enumerated(EnumType.ORDINAL)
private Gender gender;
// ...
}
创建好实体类后,Spring 启动时会自动扫描实体类,并按照配置中设置好的 DLL 模式(这里是update
)来处理表结构。
除了上边这些 JPA 相关的注解,实体类往往还需要实现 Getter/Setter/hashCode/toString/equals等常用方法,这些都可以利用 Lombok 的相关注解完成创建,此处不再展示,感兴趣的可以查看完整示例。
Repository
JPA 的相关 API 通过 Repository
接口操作数据库(类似于MyBatis的Mapper
),并且提供一些基础的功能性 Repository
供我们使用和扩展:
CrudRepository
,提供基本的 CRUD 操作。PagingAndSortingRepository
,提供分页和排序操作。ListCrudRepository
,在CrudRepository
基础上提供列表相关操作。ListPagingAndSortingRepository
,在PagingAndSortingRepository
基础上提供列表相关操作。JpaRepository
,提供所有以上操作。
一般而言,只需要让自定义接口扩展JpaRepository
即可:
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
}
Service
Service 层可以依赖注入 Repository 后查询数据库:
@Service
public class StudentService {
@Autowired
private StudentRepository studentRepository;
public List<Student> list(){
return studentRepository.findAll();
}
}
Tests
编写测试用例:
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@SpringJUnitWebConfig(classes = {JpaApplication.class})
@TestPropertySource("classpath:application.properties")
public class StudentServiceTests {
@Autowired
private StudentService studentService;
@Autowired
private StudentRepository studentRepository;
private List<Student> students = List.of(
new Student("icexmoon", LocalDate.of(1989, 10, 1), Gender.MALE),
new Student("JackChen", LocalDate.of(1990, 5, 1), Gender.MALE),
new Student("HanMeimei", LocalDate.of(1991, 6, 1), Gender.FEMALE));
@BeforeEach
void beforeEach() {
studentRepository.deleteAll();
for(var student: students){
studentRepository.save(student);
}
}
@Test
void testList() {
List<Student> students = studentService.list();
Assertions.assertEquals(this.students, students);
}
@AfterEach
void afterEach(){
studentRepository.deleteAll();
}
}
因为这里涉及数据库,所以利用@BeforeEach
在每次运行测试用例前向表中添加测试数据,并在@AfterEach
方法中清空表中的数据。这么做是为了让每个测试用例在运行前都有相同的测试数据环境。
当然,这么做比较繁琐,使用事务会让事情简单很多:
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@SpringJUnitWebConfig(classes = {JpaApplication.class})
@TestPropertySource("classpath:application.properties")
@Transactional
public class StudentServiceV2Tests {
@Autowired
private StudentService studentService;
@Autowired
private StudentRepository studentRepository;
private final List<Student> students = List.of(
new Student("icexmoon", LocalDate.of(1989, 10, 1), Gender.MALE),
new Student("JackChen", LocalDate.of(1990, 5, 1), Gender.MALE),
new Student("HanMeimei", LocalDate.of(1991, 6, 1), Gender.FEMALE));
@BeforeEach
void beforeEach() {
studentRepository.deleteAll();
studentRepository.saveAll(students);
}
@Test
void testList() {
List<Student> students = studentService.list();
Assertions.assertEquals(this.students, students);
}
}
@Transactional
可以为当前的测试套件(Test Suite)开启事务支持,并且测试类中的每个测试用例(@Test
)都将在事务中运行,且在执行完毕后自动执行事务回滚。
特别的,测试用例生命周期方法(@BeforeEach
和@AfterEach
)同样会包括在测试用例事务中,因此在这里可以将清理和添加测试数据的步骤添加在@beforeEach
方法中。
- 相应的,测试套件(测试类)的生命周期方法(
@BeforeAll
和@AfterAll
)不会被包含在事务中。- 如果想让某个测试用例执行后不回滚,可以添加
@Commit
注解。
The End,谢谢阅读。
可以从这里获取本文的完整示例。
参考资料
- Spring JUnit Jupiter Testing Annotations :: Spring Framework
- Transaction Management :: Spring Framework
- Context Management :: Spring Framework
- TestTransaction (Spring Framework 6.0.10 API)
- Programmatic Transactions in the TestContext Framework | Baeldung
- Learn JPA & Hibernate | Baeldung
- UUID如何保证唯一性? - 知乎 (zhihu.com)
- Hibernate – Mapping Date and Time | Baeldung
- 从零开始 Spring Boot 33:Null-safety - 红茶的个人站点 (icexmoon.cn)
- 从零开始 Spring Boot 35:Lombok - 红茶的个人站点 (icexmoon.cn)
- Spring Boot with Hibernate | Baeldung
- Bootstrapping Hibernate 5 with Spring | Baeldung