从零开始 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


















