SpringBoot 整合 JPA 轻松搞定数据表增删改查!

01、背景介绍

在之前的文章中,我们介绍了通过JdbcTemplate来实现数据库的访问和读写操作。当有一定的开发经验之后,你会发现所有涉及到数据库操作的代码,除了表名和字段不同外,操作的语句基本都类似,功能上可以统一归纳为“增、删、改、查”,编写大量这种类似的代码,对于开发者来说,其实非常枯燥。

为了解决重复的编写数据操作语句,开发社区诞生了许多优秀的 ORM 框架,比如 Hibernate、OpenJPA、TopLink 等,其中 Hibernate 相对较为知名,在 Hibernate 框架的帮助下,开发者可以轻松的以操作 Java 实体的方式来完成对数据表的“增删改查”操作,能极大的简化代码编写的工作量。

国内外有不少的项目基于 Spring Boot JPA 来完成对数据库的操作访问,那么 Spring Boot JPA 和 ORM 框架之间有着怎样的关系呢?

简单的说,Spring Boot JPA 是 Spring 在 ORM 框架的基础上封装的一套 JPA 应用框架,具体的数据访问和操作实现还是依赖于 ORM 框架来完成,Spring Boot JPA 只是完成了接口操作的标准封装,包括增删改查等在内的常用功能,可以帮助开发者降低学习成本,同时极大的提升开发效率。

值得一提的是:JPA (Java Persistence API) 是 Sun 官方提出的一套 Java 数据持久化操作的规范,不是一套产品,像上文说的 Hibernate,OpenJPA,TopLink 等,可以理解成 JPA 的具体产品实现。

以 Spring Boot 的2.0版本为例,Spring Boot JPA 的底层依赖于 Hibernate 框架来完成数据库的访问和操作,如果你熟悉 Hibernate 的框架使用,那么可以轻松的上手并使用它。

下面我们一起来看看 Spring Boot JPA 的具体使用姿势。
02、应用实践

2.1、工程配置

首先在pom.xml文件中添加相关的依赖包。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

然后在application.properties文件中配置相关的数据源访问地址,以及相关hibernate属性配置。

# 数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# hibernate信息配置
spring.jpa.properties.hibernate.hbm2ddl.auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql=true

其中spring.jpa.properties.hibernate.hbm2ddl.auto参数是hibernate的一个配置属性,主要作用有:自动创建、更新、验证数据库表结构。

相关的可选参数如下:

  • create:每次加载 hibernate 相关实体表时会删除上一次生成的表,然后按照最新的 model 类生成新表,会造成数据库表数据丢失;
  • create-drop:每次加载 hibernate 时会根据 model 类生成新表,当服务关闭时,表自动删除,通常用于测试;
  • update:常用属性,第一次加载 hibernate 时会根据 model 类自动建表,以后加载 hibernate 时根据 model 类自动更新表结构,但是不会删除表中的数据;
  • validate:每次加载 hibernate 时会验证数据库表的结构,只会和数据库中的表进行比较,不会创建新表,但是会插入新值;

其次,spring.jpa.properties.hibernate.dialect参数主要用于指定生成表名时的存储引擎为 InnoDBD,如果不配置,默认是MylSAMspring.jpa.show-sql参数用于打印出自动生成的 SQL,方便调试。

2.2、基本增删改查操作

下面我们一起来体验一下 JPA 中常用的增删改查操作。

2.2.1、创建实体

创建一个Student实体,包含三个属性,表名为tb_student,当加载 hibernate 的时候会自动创建到数据库中,代码如下:

@Entity
@Table(name = "tb_student")
public class Student {
    @Id
    @GeneratedValue
    private Long id;
    @Column(nullable = false, unique = true)
    private String name;
    @Column(nullable = false)
    private Integer age;
    // set、get方法等...
}
  • @Entity注解用于标识Student类是一个持久化的实体类;
  • @Table注解用于标识Student类映射到数据库中的表名称;
  • @Id注解用于标识Student映射到数据库的主键字段
  • @GeneratedValue注解用于标识Student映射到数据库的主键为自增类型
  • @Column注解用于标识Student映射到数据库的字段相关信息

    2.2.2、创建数据访问接口

针对Student实体类,创建一个对应的JpaRepository接口,用于实现对实体的数据访问和操作,代码如下:

public interface StudentRepository extends JpaRepository<Student,Long> {
}

其中JpaRepository接口已经封装好了常用的增删改查方法逻辑,使用者只需要调用相关的方法接口实现对数据库表的操作。

JpaRepository接口封装的部分方法,源码如下图!

#### 2.2.3、单元测试

完成以上的实体创建和数据访问接口的编写之后,下面我们编写对应的单元测试类来验证一下编写的内容是否正确,代码如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class StudentJPATest {
    @Autowired
    private StudentRepository studentRepository;
    @Test
    public void test(){
        // 插入3条数据
        studentRepository.save(new Student("张三", 20));
        studentRepository.save(new Student("李四", 21));
        studentRepository.save(new Student("王五", 22));
        // 查询全部数据
        List<Student> dbList = studentRepository.findAll();
        System.out.println("第一次全量查询结果:" + dbList.toString());
        System.out.println("------------------------");
        // 修改数据
        studentRepository.save(new Student(dbList.get(0).getId(),"赵六", 20));
        // 查询指定数据
        Optional<Student> findResult = studentRepository.findById(dbList.get(0).getId());
        System.out.println("查询第一条数据结果:" + findResult.toString());
        System.out.println("-----------------");
        // 删除数据
        studentRepository.deleteById(dbList.get(0).getId());
        // 查询全部数据
        List<Student> result = studentRepository.findAll();
        System.out.println("第二次全量查询结果:" + result.toString());
    }
}

运行单元测试,输出结果如下!

第一次全量查询结果:[Student{id=1, name='张三', age=20}, Student{id=2, name='李四', age=21}, Student{id=3, name='王五', age=22}]
------------------------
查询第一条数据结果:Optional[Student{id=1, name='赵六', age=20}]
------------------------
第二次全量查询结果:[Student{id=2, name='李四', age=21}, Student{id=3, name='王五', age=22}]

2.3、自定义简单查询操作

Spring Boot JPA 不仅为开发者封装了常用的模板方法,还支持根据方法名来动态生成 SQL 语句,比如findByXXcountByXXgetByXX后面跟属性名称,当调用方法的时候会自动生成响应的 SQL 语句,具体示例如下:

public interface StudentRepository extends JpaRepository<Student,Long> {
    /**
     * 自定义简单查询,通过姓名进行搜索
     * @param name
     * @return
     */
    Student findByName(String name);
    /**
     * 自定义简单查询,通过姓名和年龄进行统计
     * @param name
     * @return
     */
    Integer countByNameAndAge(String name, Integer age);
}

编写单元测试验证内容的正确性。

@RunWith(SpringRunner.class)
@SpringBootTest
public class StudentJPATest {
    @Autowired
    private StudentRepository studentRepository;
    @Test
    public void simpleTest(){
        Student result1 = studentRepository.findByName("李四");
        System.out.println("第一次查询结果:" + result1.toString());
        System.out.println("-----------------");
        Integer result2 = studentRepository.countByNameAndAge("王五", 22);
        System.out.println("第二次查询结果:" + result2);
    }
}

输出结果如下!

第一次查询结果:Student{id=2, name='李四', age=21}
-----------------
第二次查询结果:1

方法上支持 SQL 语句中的关键字,比如AndOrLikeOrderBy等。

具体关键字上的使用和生成的 SQL 对应的关系如下:

更多的关键字使用可以参阅官方文档。

2.4、复杂查询操作

在实际的开发过程中,由于业务的需要,我们经常需要编写复杂的 SQL 语句,比如链表查询,分页查询等,这个时候就需要用到自定义 SQL 语句的操作了。

2.4.1、自定义 SQL 查询

其实大部分的 SQL 语句都可以通过方法来动态生成,如果想自定义 SQL 查询,Spring Boot JPA 也是支持的,操作上很简单。

在接口方法上,添加@Query注解,即可实现自定义 SQL 语句;如果涉及到新增、修改和删除操作,需要再加上@Modifying注解,同时也需要添加@Transactional注解事务支持。

具体示例如下:

public interface StudentRepository extends JpaRepository<Student,Long> {
    /**
     * 自定义SQL语句,单条查询
     * @param studentName
     * @return
     */
    @Query(value = "select s from Student s where s.name = ?1")
    Student findByStudentName(String studentName);
    /**
     * 自定义SQL语句,修改数据
     * @param name
     * @param age
     * @return
     */
    @Transactional
    @Modifying
    @Query(value = "update Student s set s.age = ?2 where s.name = ?1")
    int updateAgeByName(String name, Integer age);
    /**
     * 自定义SQL语句,删除数据
     * @param name
     */
    @Transactional
    @Modifying
    @Query(value = "delete from Student s where s.name = ?1")
    int deleteByName(String name);
}

编写单元测试验证内容的正确性。

@Test
public void sqlTest(){
    // 新增
    studentRepository.save(new Student("王五", 22));
    // 查询
    Student result1 = studentRepository.findByStudentName("王五");
    System.out.println("第一次查询结果:" + result1.toString());
    System.out.println("-----------------");
    // 修改
    studentRepository.updateAgeByName("王五", 30);
    Student result2 = studentRepository.findByStudentName("王五");
    System.out.println("第二次查询结果:" + result2.toString());
    System.out.println("-----------------");
    // 删除
    studentRepository.deleteByName("王五");
    Student result3 = studentRepository.findByStudentName("王五");
    System.out.println("第三次查询结果:" + result3);
}

输出结果如下!

第一次查询结果:Student{id=4, name='王五', age=22}
-----------------
第二次查询结果:Student{id=4, name='王五', age=30}
-----------------
第三次查询结果:null

值得注意的是:这里自定义的 SQL 语句并非数据库中的 SQL 语句,而是 hibernate 所支持 SQL 语句,简称 hsql,例如表名要采用 Java 实体而非数据库中真实的表名,否则可能会报错。

2.4.2、分页查询

分页查询,在实际的业务开发中非常常见,其实 Spring Boot JPA 已经帮助开发者封装了分页查询的方法逻辑,在查询的时候传入Pageable参数即可。

示例如下:

@Test
public void pageTest(){
    // 构建分页参数
    int page=0,size=10;
    Sort sort = new Sort(Sort.Direction.DESC, "id");
    Pageable pageable = PageRequest.of(page, size, sort);
    // 构建单条件查询参数
    Student param = new Student();
    param.setAge(21);
    Example<Student> example = Example.of(param);
    // 发起分页查询
    Page<Student> result = studentRepository.findAll(example, pageable);
    System.out.println("查询结果,总行数:" + result.getTotalElements());
    System.out.println("查询结果,明细:" + result.getContent());
}

输出结果如下:

查询结果,总行数:1
查询结果,明细:[Student{id=2, name='李四', age=21}]

当然我们也可以根据自定义简单查询来实现分页查询,在 JPA 的帮助下动态生成 SQL 语句,示例如下:

public interface StudentRepository extends JpaRepository<Student,Long> {
    /**
     * 自定义简单查询,通过年龄进行分页搜索
     * @param age
     * @param pageable
     * @return
     */
    Page<Student> findByAge(Integer age, Pageable pageable);
}

值得注意的是:自定义的方法不会自动进行count语句汇总查询,推荐采用模板方法来进行分页查询。

2.4.3、多表查询

多表查询,也是实际开发中经常会碰到的场景,Spring Boot JPA 提供了两种实现方式,第一种是利用 Hibernate 的级联查询来实现,第二种是自定义 SQL 语句来实现。

第一种就不多说了,主要通过@OneToOne@OneToMany@ManyToOne@ManyToMany@JoinTable注解来完成多表的级联查询,不过这种方式需要在数据库层面建立外键关联,通过外键来完成级联查询,不推荐采用。

下面我们来介绍一下自定义 SQL 语句来实现,实现起来也很简单,示例如下:

public interface TeacherRepository extends JpaRepository<Teacher,Long> {
    /**
     * 自定义链表查询
     * @return
     */
    @Query(value = "select s.id, s.name, t.teacher_id as teacherId, t.teacher_name as teacherName from tb_student s left join tb_teacher t on s.teacher_id = t.teacher_id ", nativeQuery = true)
    List<Map<String,Object>> findCustomer();
}

编写单元测试,验证代码的正确性。

@RunWith(SpringRunner.class)
@SpringBootTest
public class TeacherJPATest {
    @Autowired
    private TeacherRepository teacherRepository;
    @Test
    public void test2(){
        // 查询全部数据
        List<Map<String,Object>> dbList = teacherRepository.findCustomer();
        System.out.println("查询结果:" + JSONObject.toJSONString(dbList));
    }
}

输出结果如下:

查询结果:[{"id":9,"teacherId":1,"name":"李1","teacherName":"张老师"},{"name":"李2","id":10,"teacherId":1,"teacherName":"张老师"},{"id":11,"name":"李3","teacherId":1,"teacherName":"张老师"}]

直接编写 sql 语句,非常简单灵活。

2.5、属性映射屏蔽操作

如果某个实体类中的属性,不想被映射到数据库,可以添加@Transient注解来实现,示例如下。

@Transient
private String  name;

03、小结

本文主要围绕利用 Spring Boot JPA 对数据库的访问和操作,做了一次知识内容的整理和总结,如果有描述不对的地方,欢迎留言指出!
04、参考

1、http://www.ityouknow.com/springboot/2016/08/20/spring-boot-jpa.html

6