红茶的个人站点

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

从零开始 Spring Boot 48:JPA & Hibernate

2023年6月25日 1266点热度 0人点赞 0条评论

spring boot

图源:简书 (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/jpa
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;
    // ...
}

最后,不要忘记确保实体类拥有一个默认构造器(无参构造器):

public class Student {
	// ...
    public Student() {
    }

    public Student(String name, LocalDate birthDay, Gender gender) {
        this.name = name;
        this.setBirthDay(birthDay);
        this.gender = gender;
    }
}

Hibernate 会使用默认构造器创建对象,如果没有,会报错。

创建好实体类后,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

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: hibernate jpa spring
最后更新:2023年6月27日

魔芋红茶

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

点赞
< 上一篇
下一篇 >

文章评论

取消回复

*

code

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

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号