Menu Close

使用 Gradle 整合 Spring Data JPA(例六)

环境描述

描述项 内容
操作系统 CentOS Linux release 7.8.2003 (Core)
java版本 java version "1.8.0_161"
IDEA版本 2022.2.2
Gradle版本 7.5 (使用了 kotlin 语言
上接文章 在IDEA中使用 Gradle 构建 Java Spring Boot项目(例五)
  • Spring引导应用程序架构图
    file

1. JPA 浅显的介绍

1.1. 什么是JPA

JPA的全称是Java Persistence API, 即Java 持久化API,是SUN公司推出的一套基于ORM的规范,内部是由一系列的接口和抽象类构成。
JPA 本质上就是一种 ORM 规范,不是ORM 框架 —— 因为 JPA 并未提供 ORM 实现,它只是制订了一些规范,提供了一些编程的 API 接口,但具体实现则由 ORM 厂商提供实现。

file

1.2. JPA的查询能力

JPA的查询语言是面向对象而非面向数据库的,它以面向对象的自然语法构造查询语句,可以看成是Hibernate HQL的等价物。JPA定义了独特的JPQL(Java Persistence Query Language),JPQL是EJB QL的一种扩展,它是针对实体的一种查询语言,操作对象是实体,而不是关系数据库的表,而且能够支持批量更新和修改、JOIN、GROUP BY、HAVING 等通常只有 SQL 才能够提供的高级查询特性,甚至还能够支持子查询。

1.3. JPA的高级特性

JPA 中能够支持面向对象的高级特性,如类之间的继承、多态和类之间的复杂关系,这样的支持能够让开发者最大限度的使用面向对象的模型设计企业应用,而不需要自行处理这些特性在关系数据库的持久化。

1.4. JPA与MyBatis

在使用CQRS(Command Query Responsibility Segregation 命令和查询职责分离)模式下,面向领域的逻辑代码应该使用JPA这样的ORM框架,它们更有利于代码的建模,并且可以提升代码的可读性。但对于面向数据的查询代码,我们应该使用MyBatis这样的持久化框架,它有利于复杂查询SQL语句的编写以及性能优化。如果是构建一个大型应用的话,这两者应该是可以同时存在的,它们分别负责提供不同场景下的持久化支持。

1.5. Spring Data JPA 是如何工作的

file

file

2. 创建一个实体类

2.1. 增补 spring-data-jpa 依赖

在文件 build.gradle.kts 中增加 spring-data-jpa 依赖。

dependencies {
  implementation("org.springframework.boot:spring-boot-starter-data-jpa:2.7.3")
}

2.2. 创建 domain.model

file

file

2.3. 在 model 目录下创建实体类——Question

实体类Question代码详情

package xin.qishuo.question.domain.model;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Question {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private String id;

    private String questionId;

    private String title;

    private String detail;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getQuestionId() {
        return questionId;
    }

    public void setQuestionId(String questionId) {
        this.questionId = questionId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDetail() {
        return detail;
    }

    public void setDetail(String detail) {
        this.detail = detail;
    }

    public Question(String questionId, String title, String detail) {
        this.questionId = questionId;
        this.title = title;
        this.detail = detail;
    }
}

2.4. 创建 domain.repository

file

file

2.5. 在 repository 目录下创建接口——QuestionRepository

QuestionRepository类代码详情

package xin.qishuo.question.domain.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import xin.qishuo.question.domain.model.Question;

public interface QuestionRepository extends JpaRepository < Question,String > {

}

JpaRepository 类图(可以使用 Intellij IDEA 的快捷键 Ctrl + Alt + Shirft + U 生成UML图)如下:

file

3. 为 repository 编写单元测试

3.1. 快速创建 QuestionRepositoryTest 测试类

file

file

3.2. QuestionRepositoryTest 测试类的代码

QuestionRepositoryTest测试类代码详情

package xin.qishuo.question.domain.repository;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.context.SpringBootTest;
import xin.qishuo.question.domain.model.Question;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

@SpringBootTest
class QuestionRepositoryTest {

    @Autowired
    private QuestionRepository questionRepository;

    @Test
    void repositoryShouldSuccessfulSaveQuestion() {
        Question question = new Question("UID_00001", "A test tile", "A test detail");
        Question savedQuestion = questionRepository.save(question);
        System.out.println(savedQuestion.getQuestionId());
        System.out.println(savedQuestion.getTitle());
        System.out.println(savedQuestion.getDetail());
        assertThat(savedQuestion.getId(),is(notNullValue()));
        assertThat(savedQuestion.getQuestionId(),equalTo(question.getQuestionId()));
        assertThat(savedQuestion.getTitle(),equalTo(question.getTitle()));
        assertThat(savedQuestion.getDetail(),equalTo(question.getDetail()));
    }

}

QuestionRepository对象需要由Spring注入,这时测试需要去启动一个Spring上下文,注解 @DataJpaTest 会只启动跟 data-jpa 相关的bean,这样的话可以使测试更省资源,更快速地完成测试。

实际运行在测试中,仍然使用了注解 @SpringBootTest,需要进一步了解 注解 @DataJpaTest 、注解 @SpringBootTest 以及注解 @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 之间的区别。

测试代码中先创建一个question对象,然后尝试保存它,保存后断言它生成了ID并且所有的字段都原封不动地保存了下来。测试代码重复执行时,会生成除 id 之外都相同的记录。

4. 数据库表的创建(这里使用MariaDB)

4.1. 增补数据库驱动依赖

dependencies {
  runtimeOnly("org.mariadb.jdbc:mariadb-java-client:3.0.6")
}

4.2. 数据库表与字段的创建脚本

CREATE TABLE question (id int NOT NULL AUTO_INCREMENT,
                       question_Id varchar(16) NOT NULL,
                       title varchar(32) NOT NULL,
                       detail varchar(521) NOT NULL,
                       PRIMARY KEY (id));

4.3. 声明数据库连接

src/main/resources 目录下新建 application.yml(Spring Boot 提供的一个配置文件) 文件,并将数据库的连接配置写入该文件中。

数据源访问方式

spring:
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: dbc:mariadb://172.16.16.16:6006/study
    username: study
    password: study

5. 执行单元测试

file

附录

附录A. 相关联的文章

附录B. 参考

附录C. 其他

题外话:JPA对复杂 SQL 的支持不好,没有实体关联的两个表要做 join,这也是它最大的诟病,但其设计理念上本身如此:

现代微服务的架构,各个服务之间的数据库是隔离的,跨越很多张表的 join 操作本就不应该交给单一的业务数据库去完成。参考:为什么强烈建议你不要做联表查询?

解决方案是:使用 elasticSearch做视图查询 或者 mongodb 一类的Nosql 去完成。问题本不是问题。

本质上看,JPA是面向对象的,而MyBatis是面向关系的。换言之,JPA是以面向对象的领域模型为中心的,而MyBatis是以数据库为中心的。领域模型致力于解决业务逻辑问题,而关系模型致力于解决数据的高效存取问题。