本文主要介绍 Spring Boot 中如何使用 Sping Data JPA,相关的环境及软件信息如下:Spring Boot 2.6.10。

1、Sping Data JPA 简介

Spring Data JPA 是 Spring Data 家族的一部分,它对基于 JPA 的数据访问提供了增强支持,让 JPA 更加易用。 它使得使用 Spring 构建的应用程序访问数据库变得更加容易。

编写应用程序的数据访问层是一件很麻烦的事, 必须编写太多样板代码来执行简单的查询以及分页和审计。 Spring Data JPA 旨在通过减工作量来显着改进数据访问层的实现。 作为开发人员,编写数据访问层接口,包括自定义查询方法,Spring 将自动提供实现。

1.1、Spring Data JPA 的特点

  • Sophisticated support to build repositories based on Spring and JPA
  • Support for​​Querydsl​​ predicates and thus type-safe JPA queries
  • Transparent auditing of domain class
  • Pagination support, dynamic query execution, ability to integrate custom data access code
  • Validation of​​@Query​​ annotated queries at bootstrap time
  • Support for XML based entity mapping
  • JavaConfig based repository configuration by introducing​​@EnableJpaRepositories​​.

1.2、Spring Data JPA 与 Hibernate 的关系

 Spring Data JPA 是 Spring 提供的一套对 JPA 操作更加高级的封装,底层依赖 Hibernate 的 JPA 实现。

2、JpaRepository 和 JpaSpecificationExecutor 接口

Spring Data JPA 提供了 JpaRepository 和 JpaSpecificationExecutor 接口,通过它们可以快速定义 DAO 接口,Spring Data JPA 会自动实现该接口。

2.1、JpaRepository

JpaRepository 接口内置了很多方法:

JPA 入门实战(4)--Spring Data JPA 使用_ORM

如果不满足业务需求,可以自定义方法。

2.1.1、使用 Query 注解

org.springframework.data.jpa.repository.Query 注解可以定义使用 JPQL 或 SQL 操作数据。 

/**
* 通过 JPQL 查询
*/
@Query("from Student where name=?1 and age=?2")
Student query(String name, Integer age);

/**
* SQL 语句查询数据
*/
@Query(value = "select * from a_student where name=? and age=?", nativeQuery = true)
Student queryBySql(String name, Integer age);

2.1.1、按照 Spring Data JPA 规范定义方法名称

Spring Data JPA 提供一些关键词,通过这些关键词和实体属性名称来组装方法名称。

主题关键词:

Keyword

Description

​find…By​​​, ​​read…By​​​, ​​get…By​​​, ​​query…By​​​, ​​search…By​​​, ​​stream…By​

General query method returning typically the repository type, a ​​Collection​​​ or ​​Streamable​​​ subtype or a result wrapper such as ​​Page​​​, ​​GeoResults​​​ or any other store-specific result wrapper. Can be used as ​​findBy…​​​, ​​findMyDomainTypeBy…​​ or in combination with additional keywords.

​exists…By​

Exists projection, returning typically a ​​boolean​​ result.

​count…By​

Count projection returning a numeric result.

​delete…By​​​, ​​remove…By​

Delete query method returning either no result (​​void​​) or the delete count.

​…First<number>…​​​, ​​…Top<number>…​

Limit the query results to the first ​​<number>​​​ of results. This keyword can occur in any place of the subject between ​​find​​​ (and the other keywords) and ​​by​​.

​…Distinct…​

Use a distinct query to return only unique results. Consult the store-specific documentation whether that feature is supported. This keyword can occur in any place of the subject between ​​find​​​ (and the other keywords) and ​​by​​.

Logical keyword

Keyword expressions

​AND​

​And​

​OR​

​Or​

​AFTER​

​After​​​, ​​IsAfter​

​BEFORE​

​Before​​​, ​​IsBefore​

​CONTAINING​

​Containing​​​, ​​IsContaining​​​, ​​Contains​

​BETWEEN​

​Between​​​, ​​IsBetween​

​ENDING_WITH​

​EndingWith​​​, ​​IsEndingWith​​​, ​​EndsWith​

​EXISTS​

​Exists​

​FALSE​

​False​​​, ​​IsFalse​

​GREATER_THAN​

​GreaterThan​​​, ​​IsGreaterThan​

​GREATER_THAN_EQUALS​

​GreaterThanEqual​​​, ​​IsGreaterThanEqual​

​IN​

​In​​​, ​​IsIn​

​IS​

​Is​​​, ​​Equals​​, (or no keyword)

​IS_EMPTY​

​IsEmpty​​​, ​​Empty​

​IS_NOT_EMPTY​

​IsNotEmpty​​​, ​​NotEmpty​

​IS_NOT_NULL​

​NotNull​​​, ​​IsNotNull​

​IS_NULL​

​Null​​​, ​​IsNull​

​LESS_THAN​

​LessThan​​​, ​​IsLessThan​

​LESS_THAN_EQUAL​

​LessThanEqual​​​, ​​IsLessThanEqual​

​LIKE​

​Like​​​, ​​IsLike​

​NEAR​

​Near​​​, ​​IsNear​

​NOT​

​Not​​​, ​​IsNot​

​NOT_IN​

​NotIn​​​, ​​IsNotIn​

​NOT_LIKE​

​NotLike​​​, ​​IsNotLike​

​REGEX​

​Regex​​​, ​​MatchesRegex​​​, ​​Matches​

​STARTING_WITH​

​StartingWith​​​, ​​IsStartingWith​​​, ​​StartsWith​

​TRUE​

​True​​​, ​​IsTrue​

​WITHIN​

​Within​​​, ​​IsWithin​

 

详细说明可参考官网文档:https://docs.spring.io/spring-data/jpa/docs/2.6.6/reference/html/#repository-query-keywords。

命名样例:

方法名

JPQL

findByLastnameAndFirstname

… where x.lastname = ?1 and x.firstname = ?2

findByLastnameOrFirstname

… where x.lastname = ?1 or x.firstname = ?2

findByStartDateBetween

… where x.startDate between ?1 and ?2

findByAgeLessThan

… where x.age < ?1

findByAgeLessThanEqual

… where x.age ⇐ ?1

2.2、JpaSpecificationExecutor

JpaSpecificationExecutor 接口主要用来在 JpaRepository 接口无法满足要求时,可以使用 JPA 的标准 API 来操作数据。

JPA 入门实战(4)--Spring Data JPA 使用_spring_02

3、Sping Data JPA 使用

这里演示下 Spring Data JPA 的基本使用,工程目录结构如下:

JPA 入门实战(4)--Spring Data JPA 使用_ORM_03

3.1、引入依赖

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.10</version>
<relativePath />
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

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

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
</dependencies>

3.2、创建实体类

package com.abc.demojpa.entity;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import javax.persistence.*;
import java.time.LocalDateTime;

@NoArgsConstructor
@Data
@Entity
@Table(name = "a_student")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@CreationTimestamp
@Column(name = "create_time")
private LocalDateTime createTime;

@UpdateTimestamp
@Column(name = "modify_time")
private LocalDateTime modifyTime;

private String name;

private Integer age;

@Column(name = "home_address")
private String homeAddress;
}

3.3、编写配置文件(application.yml)

spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://10.49.196.10:3306/test?useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
jpa:
hibernate:
ddl-auto: update

3.4、定义 DAO 接口

package com.abc.demojpa.dao;

import com.abc.demojpa.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface IStudentRepository extends JpaRepository<Student, Long>, JpaSpecificationExecutor<Student> {
/**
* 根据姓名和年龄查询
*/
List<Student> findByNameAndAge(String name, Integer age);

/**
* 根据姓名和年龄查询前5条
*/
List<Student> findTop5ByNameAndAge(String name, Integer age);

/**
* 根据姓名和年龄查询并按id排序
*/
List<Student> findByNameAndAgeOrderByIdAsc(String name, Integer age);

/**
* 根据姓名和年龄查询第一条
*/
Student findFirstByNameAndAge(String name, Integer age);

/**
* 通过 JPQL 查询
*/
@Query("from Student where name=?1 and age=?2")
Student query(String name, Integer age);

/**
* 通过 JPQL 查询
*/
@Query("from Student where name=:name and age=:age")
Student query2(@Param("name") String name, @Param("age") Integer age);

/**
* 通过 JPQL 更新数据
*/
@Query("update Student set homeAddress=?1 where id=?2")
@Modifying
void updateHomeAddress(String homeAddress, Long id);

/**
* SQL 语句查询数据
*/
@Query(value = "select * from a_student where name=? and age=?", nativeQuery = true)
Student queryBySql(String name, Integer age);
}

3.5、编写启动类

package com.abc.demojpa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class DemoJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DemoJpaApplication.class, args);
}
}

3.6、编写测试用例

3.6.1、增加数据

/**
* 增加数据
* Spring Boot 事务测试时默认会回滚操作避免产生测试数据,如果不需要回滚可使用 @Commit 注解
*/
@Test
@Transactional
@Commit
public void add() {
Student student = new Student();
student.setName("小明");
student.setAge(15);
student.setHomeAddress("江苏");

Student student2 = new Student();
student2.setName("小红");
student2.setAge(18);
student2.setHomeAddress("广东");

studentRepository.save(student);
studentRepository.save(student2);
}

3.6.2、修改数据

@Transactional
@Commit
@Test
public void modify() {
Student student = new Student();
student.setId(3L);
student.setName("小明2");
student.setAge(15);
student.setHomeAddress("江苏");
//设置了 id 表示更新数据
studentRepository.save(student);

//调用自定义的更新方法
studentRepository.updateHomeAddress("上海", 12L);
}

3.6.3、查询数据

@Test
public void query() {
//根据 id 查询
Optional<Student> optional = studentRepository.findById(12L);
logger.info("student={}", optional.get());

//查询所有
List<Student> list = studentRepository.findAll();
logger.info("list={}", list);

//分页及排序查询
List<Sort.Order> orders = new ArrayList<>();
orders.add(new Sort.Order(Sort.Direction.DESC, "id"));
orders.add(new Sort.Order(Sort.Direction.ASC, "name"));
Page<Student> page = studentRepository.findAll(PageRequest.of(0, 10, Sort.by(orders)));
logger.info("page.getTotalElements={},page.getTotalPages()={},page.toList()={}", page.getTotalElements(), page.getTotalPages(), page.toList());

//通过 JPA 标准 API 查询,可以用来动态拼接查询条件
Specification<Student> specification = new Specification<Student>() {
@Override
public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
List<Predicate> list = new ArrayList<>();
list.add(criteriaBuilder.equal(root.get("name"), "小红"));
list.add(criteriaBuilder.ge(root.get("age"), 10));
return criteriaBuilder.and(list.toArray(new Predicate[]{}));
}
};
list = studentRepository.findAll(specification);
logger.info("list={}", list);

list = studentRepository.findByNameAndAge("小红", 18);
logger.info("list={}", list);

list = studentRepository.findTop5ByNameAndAge("小红", 18);
logger.info("list={}", list);

list = studentRepository.findByNameAndAgeOrderByIdAsc("小红", 18);
logger.info("list={}", list);

Student student = studentRepository.findFirstByNameAndAge("小红", 18);
logger.info("student={}", student);

student = studentRepository.query("小红", 18);
logger.info("student={}", student);

student = studentRepository.query2("小红", 18);
logger.info("student={}", student);

student = studentRepository.queryBySql("小红", 18);
logger.info("student={}", student);
}

3.6.4、删除数据

@Test
public void remove() {
//根据实体删除
Student student = new Student();
student.setId(3L);
studentRepository.delete(student);

List<Student> students = new ArrayList<>();
Student student2 = new Student();
student.setId(4L);
students.add(student);
students.add(student2);
studentRepository.deleteAll(students);

//根据 id 删除
studentRepository.deleteById(5L);
List<Long> ids = new ArrayList<Long>(){{
add(6L);
add(7L);
}};
studentRepository.deleteAllByIdInBatch(ids);
}

3.6.5、完整代码

JPA 入门实战(4)--Spring Data JPA 使用_数据_04

JPA 入门实战(4)--Spring Data JPA 使用_spring_05

package com.abc.demojpa.service;


import com.abc.demojpa.dao.IStudentRepository;
import com.abc.demojpa.entity.Student;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.test.annotation.Commit;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestService {
private static final Logger logger = LoggerFactory.getLogger(TestService.class);

@Autowired
private IStudentRepository studentRepository;

/**
* 增加数据
* Spring Boot 事务测试时默认会回滚操作避免产生测试数据,如果不需要回滚可使用 @Commit 注解
*/
@Test
@Transactional
@Commit
public void add() {
Student student = new Student();
student.setName("小明");
student.setAge(15);
student.setHomeAddress("江苏");

Student student2 = new Student();
student2.setName("小红");
student2.setAge(18);
student2.setHomeAddress("广东");

studentRepository.save(student);
studentRepository.save(student2);
}

/**
* 修改数据
*/
@Transactional
@Commit
@Test
public void modify() {
Student student = new Student();
student.setId(3L);
student.setName("小明2");
student.setAge(15);
student.setHomeAddress("江苏");
//设置了 id 表示更新数据
studentRepository.save(student);

//调用自定义的更新方法
studentRepository.updateHomeAddress("上海", 12L);
}


/**
* 查询数据
*/
@Test
public void query() {
//根据 id 查询
Optional<Student> optional = studentRepository.findById(12L);
logger.info("student={}", optional.get());

//查询所有
List<Student> list = studentRepository.findAll();
logger.info("list={}", list);

//分页及排序查询
List<Sort.Order> orders = new ArrayList<>();
orders.add(new Sort.Order(Sort.Direction.DESC, "id"));
orders.add(new Sort.Order(Sort.Direction.ASC, "name"));
Page<Student> page = studentRepository.findAll(PageRequest.of(0, 10, Sort.by(orders)));
logger.info("page.getTotalElements={},page.getTotalPages()={},page.toList()={}", page.getTotalElements(), page.getTotalPages(), page.toList());

//通过 JPA 标准 API 查询,可以用来动态拼接查询条件
Specification<Student> specification = new Specification<Student>() {
@Override
public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
List<Predicate> list = new ArrayList<>();
list.add(criteriaBuilder.equal(root.get("name"), "小红"));
list.add(criteriaBuilder.ge(root.get("age"), 10));
return criteriaBuilder.and(list.toArray(new Predicate[]{}));
}
};
list = studentRepository.findAll(specification);
logger.info("list={}", list);

list = studentRepository.findByNameAndAge("小红", 18);
logger.info("list={}", list);

list = studentRepository.findTop5ByNameAndAge("小红", 18);
logger.info("list={}", list);

list = studentRepository.findByNameAndAgeOrderByIdAsc("小红", 18);
logger.info("list={}", list);

Student student = studentRepository.findFirstByNameAndAge("小红", 18);
logger.info("student={}", student);

student = studentRepository.query("小红", 18);
logger.info("student={}", student);

student = studentRepository.query2("小红", 18);
logger.info("student={}", student);

student = studentRepository.queryBySql("小红", 18);
logger.info("student={}", student);
}

/**
* 删除数据
*/
@Test
public void remove() {
//根据实体删除
Student student = new Student();
student.setId(3L);
studentRepository.delete(student);

List<Student> students = new ArrayList<>();
Student student2 = new Student();
student.setId(4L);
students.add(student);
students.add(student2);
studentRepository.deleteAll(students);

//根据 id 删除
studentRepository.deleteById(5L);
List<Long> ids = new ArrayList<Long>(){{
add(6L);
add(7L);
}};
studentRepository.deleteAllByIdInBatch(ids);
}
}

TestService.java