Spring Data JPA 原理与实战第六天 JpaSpecificationExecutor详解

09 JpaSpecificationExecutor 解决了哪些问题?

欢迎来到第二个模块,从这一课时开始,我们就要进入高级用法与实战的学习。在进阶高级开发 / 架构师的路上,我将尽可能把经验都传授给你,帮助你少走弯路。

学习完前面 8 个课时,相信作为一名开发人员,你对 JPA 的基本用法已经有了一定的了解。那么从这一课时开始,我们要介绍一些复杂场景的使用,特别是作为一名架构师必须要掌握的内容。

我们先来看看除了前几节课我们讲解的 Define Query Method 和 @Query 之外,还有哪些查询方法。首先看一个简单的 QueryByExampleExecutor 用法。

QueryByExampleExecutor用法

QueryByExampleExecutor(QBE)是一种用户友好的查询技术,具有简单的接口,它允许动态查询创建,并且不需要编写包含字段名称的查询。

下面是一个 UML 图,你可以看到 QueryByExampleExecutor 是 JpaRepository 的父接口,也就是 JpaRespository 里面继承了 QueryByExampleExecutor 的所有方法。

Drawing 0.png

图一:Repository 类图

QBE 的基本语法

QBE 的基本语法可以分为下述几种。

public interface QueryByExampleExecutor<T> { 
 //根据“实体”查询条件,查找一个对象
<S extends T> S findOne(Example<S> example);
//根据“实体”查询条件,查找一批对象
<S extends T> Iterable<S> findAll(Example<S> example); 
//根据“实体”查询条件,查找一批对象,可以指定排序参数
<S extends T> Iterable<S> findAll(Example<S> example, Sort sort);
 //根据“实体”查询条件,查找一批对象,可以指定排序和分页参数 
<S extends T> Page<S> findAll(Example<S> example, Pageable pageable);
//根据“实体”查询条件,查找返回符合条件的对象个数
<S extends T> long count(Example<S> example); 
//根据“实体”查询条件,判断是否有符合条件的对象
<S extends T> boolean exists(Example<S> example); 
}

你可以看到这几个语法其实差不多,下面我们用 Page<S> findAll 写一个分页查询的例子,看一下效果。

QueryByExampleExecutor 的使用案例

我们还用先前的 User 实体和 UserAddress 实体,并把 User 变丰富一点,这样方便测试。两个实体关键代码如下。

@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "address")
public class User implements Serializable {
   @Id
   @GeneratedValue(strategy= GenerationType.AUTO)
   private Long id;
   private String name;
   private String email;
   @Enumerated(EnumType.STRING)
   private SexEnum sex;
   private Integer age;
   private Instant createDate;
   private Date updateDate;
   @OneToMany(mappedBy = "user",fetch = FetchType.EAGER,cascade = {CascadeType.ALL})
   private List<UserAddress> address;
}
enum SexEnum {
   BOY,GIRL
}
//User实体我们扩充了一些字段去了不同的类型,方便测试
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "user")
public class UserAddress {
   @Id
   @GeneratedValue(strategy= GenerationType.AUTO)
   private Long id;
   private String address;
   @ManyToOne(cascade = CascadeType.ALL)
   @JsonIgnore
   private User user;
}
//UserAddress基本上不变

可以看出两个实体我们加了些字段。UserAddressRepository 继承 JpaRepository,从而也继承了 QueryByExampleExceutor 里面的方法,如下所示。

public interface UserAddressRepository extends JpaRepository<UserAddress,Long> {
}

那么我们写一个测试用例,来熟悉一下 QBE 的语法,看一下完整的测试用例的写法。

package com.example.jpa.example1;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.test.annotation.Rollback;
import javax.transaction.Transactional;
import java.time.Instant;
import java.util.Date;
@DataJpaTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class UserAddressRepositoryTest {
   @Autowired
   private UserAddressRepository userAddressRepository;
   private Date now = new Date();
   /**
    * 负责添加数据,假设数据库里面已经有的数据
    */
   @BeforeAll
   @Rollback(false)
   @Transactional
   void init() {
      User user = User.builder()
            .name("jack")
            .email("123456@126.com")
            .sex(SexEnum.BOY)
            .age(20)
            .createDate(Instant.now())
            .updateDate(now)
            .build();
      userAddressRepository.saveAll(Lists.newArrayList(UserAddress.builder().user(user).address("shanghai").build(),
            UserAddress.builder().user(user).address("beijing").build()));
   }
   @Test
   @Rollback(false)
   public void testQBEFromUserAddress() throws JsonProcessingException {
      User request = User.builder()
            .name("jack").age(20).email("12345")
            .build();
      UserAddress address = UserAddress.builder().address("shang").user(request).build();
      ObjectMapper objectMapper = new ObjectMapper();
//    System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(address)); //可以打印出来看看参数是什么
//创建匹配器,即如何使用查询条件
      ExampleMatcher exampleMatcher = ExampleMatcher.matching()
            .withMatcher("user.email", ExampleMatcher.GenericPropertyMatchers.startsWith())
            .withMatcher("address", ExampleMatcher.GenericPropertyMatchers.startsWith());
      Page<UserAddress> u = userAddressRepository.findAll(Example.of(address,exampleMatcher), PageRequest.of(0,2));
    System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(u));
   }
}

其中,方法 testQBEFromUserAddress 负责测试 QBE,那么假设我们要写 API 的话,前端给我们的查询参数如下。

{
   "id" : null,
   "address" : "shang",
   "user" : {
      "id" : null,
      "name" : "jack",
      "email" : "12345",
      "sex" : null,
      "age" : 20,
      "createDate" : null,
      "updateDate" : null
   }
}

想要满足 email 前缀匹配、地址前缀匹配的动态查询条件,我们可以跑一下测试用例看一下结果。

Hibernate: select useraddres0_.id as id1_2_, useraddres0_.address as address2_2_, useraddres0_.user_id as user_id3_2_ from user_address useraddres0_ inner join user user1_ on useraddres0_.user_id=user1_.id where user1_.age=20 and (user1_.email like ? escape ?) and user1_.name=? and (useraddres0_.address like ? escape ?) limit ?
2020-09-20 23:04:24.391 TRACE 62179 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [12345%]
2020-09-20 23:04:24.391 TRACE 62179 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [CHAR] - [\]
2020-09-20 23:04:24.392 TRACE 62179 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [VARCHAR] - [jack]
2020-09-20 23:04:24.392 TRACE 62179 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [VARCHAR] - [shang%]
2020-09-20 23:04:24.393 TRACE 62179 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [5] as [CHAR] - [\]

其中我们可以看到,传进来的参数和最终执行的 SQL,还挺符合我们的预期的,所以我们也能得到正确响应的查询结果,如下图:

Drawing 1.png

也就是一个地址带一个 User 结果。

那么接下来我们分析一下 Example 这个参数,看看它具体的语法是什么。

Example 语法详解

关于 Example 的语法,我们直接看一下它的源码吧,比较简单。

public interface Example<T> {
   static <T> Example<T> of(T probe) {
      return new TypedExample<>(probe, ExampleMatcher.matching());
   }
   static <T> Example<T> of(T probe, ExampleMatcher matcher) {
      return new TypedExample<>(probe, matcher);
   }
   //实体参数
   T getProbe();
   //匹配器
   ExampleMatcher getMatcher();
   //回顾一下我们上一课时讲解的类型,这个是返回实体参数的Class Type;
   @SuppressWarnings("unchecked")
   default Class<T> getProbeType() {
      return (Class<T>) ProxyUtils.getUserClass(getProbe().getClass());
   }
}

而 TypedExample 这个类不是 public 的,看如下源码。

@ToString
@EqualsAndHashCode
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
@Getter
class TypedExample<T> implements Example<T> {
   private final @NonNull T probe;
   private final @NonNull ExampleMatcher matcher;
}

其中我们发现三个类:Probe、ExampleMatcher 和 Example,分别做如下解释:

  • Probe:这是具有填充字段的域对象的实际实体类,即查询条件的封装类(又可以理解为查询条件参数),必填。

  • ExampleMatcher:ExampleMatcher 有关如何匹配特定字段的匹配规则,它可以重复使用在多个实例中,必填。

  • Example:Example 由 Probe 探针和 ExampleMatcher 组成,它用于创建查询,即组合查询参数和参数的匹配规则。

通过 Example 的源码,我们发现想创建 Example 的话,只有两个方法:

  1. static <T> Example<T> of(T probe):需要一个实体参数,即查询的条件。而里面的 ExampleMatcher 采用默认的 ExampleMatcher.matching(); 表示忽略 Null,所有字段采用精准匹配。

  2. static <T> Example<T> of(T probe, ExampleMatcher matcher):需要两个参数构建 Example,也就表示了 ExampleMatcher 自由组合规则,正如我们上面的测试用例里面的代码一样。

那么现在又遇到个类:ExampleMatcher,我们分析一下它的语法。

ExampleMatcher 语法分析

我们通过分析 ExampleMatcher 的源码来分析一下其用法。

首先打开 Structure 视图,看看里面对外暴露的方法都有哪些。

Drawing 2.png

通过 Structure 视图可以很容易地发现,我们要关心的方法都是这些 public 类型的返回 ExampleMatcher 的方法,那么我们把这些方法搞明白了是不是就可以掌握其详细用法了呢?下面看看它的实现类。

Drawing 3.png

TypedExampleMatcher 不是 public 类型的,所以我们可以基本上不用看了,主要看一下接口里面给我们暴露了哪些实例化方法。

初始化 ExampleMatcher 实例的方法

查看初始化 ExampleMatcher 实例的方法时,我们发现只有如下三个。

先看一下前两个方法:

//默认matching方法
static ExampleMatcher matching() {
   return matchingAll();
}
//matchingAll,默认的方法
static ExampleMatcher matchingAll() {
   return new TypedExampleMatcher().withMode(MatchMode.ALL);
}

我们看到上面的两个方法所表达的意思是一样的,只不过一个是默认,一个是方法名上面有语义的。两者采用的都是 MatchMode.ALL 的模式,即 AND 模式,生成的 SQL 为如下形式:

Hibernate: select useraddres0_.id as id1_2_, useraddres0_.address as address2_2_, useraddres0_.user_id as user_id3_2_ from user_address useraddres0_ inner join user user1_ on useraddres0_.user_id=user1_.id where user1_.age=20 and user1_.name=? and (user1_.email like ? escape ?) and (useraddres0_.address like ? escape ?) limit ?

可以看到,这些查询条件之间都是 AND 的关系。

我们再看一下方法三:

static ExampleMatcher matchingAny() {
   return new TypedExampleMatcher().withMode(MatchMode.ANY);
}

第三个方法和前面两个方法的区别在于:第三个 MatchMode.ANY,表示查询条件是 or 的关系,我们看一下 SQL:

Hibernate: select count(useraddres0_.id) as col_0_0_ from user_address useraddres0_ inner join user user1_ on useraddres0_.user_id=user1_.id where useraddres0_.address like ? escape ? or user1_.age=20 or user1_.email like ? escape ? or user1_.name=?

以上就是三个初始化 ExampleMatcher 实例的方法,你在运用中需要注意 and 和 or 的关系。
那么,我们再看一下 ExampleMatcher 语法给我们暴露的方法有哪些。

ExampleMatcher 语法给我们暴露的方法

忽略大小写

关于忽略大小写,我们看下代码:

//默认忽略大小写的方式,默认 False。
ExampleMatcher withIgnoreCase(boolean defaultIgnoreCase);
//提供了一个默认的实现方法,忽略大小写;
default ExampleMatcher withIgnoreCase() {
   return withIgnoreCase(true);
}
//哪些属性的paths忽略大小写,可以指定多个参数;
ExampleMatcher withIgnoreCase(String... propertyPaths);

NULL 值的 property 怎么处理

暴露的 Null 值处理方式如下:

ExampleMatcher withNullHandler(NullHandler nullHandler);

我们直接看参数 NullHandler枚举值即可,有两个可选值:INCLUDE(包括)、IGNORE(忽略),其中要注意:

  • 标识作为条件的实体对象中,一个属性值(条件值)为 Null 时,是否参与过滤;

  • 当该选项值是 INCLUDE 时,表示仍参与过滤,会匹配数据库表中该字段值是 Null 的记录;

  • 若为 IGNORE 值,表示不参与过滤。

//提供一个默认实现方法,忽略 NULL 属性;
default ExampleMatcher withIgnoreNullValues() {
   return withNullHandler(NullHandler.IGNORE);
}
//把 NULL 属性值作为查询条件
default ExampleMatcher withIncludeNullValues() {
   return withNullHandler(NullHandler.INCLUDE);
}

到这里看一下,把 NULL 属性值作为查询条件,会执行什么样的 SQL:

Hibernate: select useraddres0_.id as id1_2_, useraddres0_.address as address2_2_, useraddres0_.user_id as user_id3_2_ from user_address useraddres0_ inner join user user1_ on useraddres0_.user_id=user1_.id where (user1_.id is null) and (user1_.update_date is null) and user1_.age=20 and (user1_.create_date is null) and lower(user1_.name)=? and (lower(user1_.email) like ? escape ?) and (user1_.sex is null) and (lower(useraddres0_.address) like ? escape ?) and (useraddres0_.id is null) limit ?

这样就会导致我们一条数据都查不出来了。
忽略某些 Paths,不参加查询条件

//忽略某些属性列表,不参与查询过滤条件。
ExampleMatcher withIgnorePaths(String... ignoredPaths);
字符串字段默认的匹配规则
ExampleMatcher withStringMatcher(StringMatcher defaultStringMatcher);

关于默认字符串的匹配方式,枚举类型有 6 个可选值,DEFAULT(默认,效果同 EXACT)、EXACT(相等)、STARTING(开始匹配)、ENDING(结束匹配)、CONTAINING(包含,模糊匹配)、REGEX(正则表达式)。
字符串匹配规则,我们和 JPQL 对应到一起举例,如下表所示:

Drawing 4.png

相关代码如下:

ExampleMatcher withMatcher(String propertyPath, GenericPropertyMatcher genericPropertyMatcher);

这里显示的是指定某些属性的匹配规则,我们看一下 GenericPropertyMatcher 是什么东西,它都提供了哪些方法。

如下图,基本可以看出来都是针对字符串属性提供的匹配规则,也就是可以通过这个方法定制不同属性的 StringMatcher 规则。

Drawing 5.png

到这里,语法部分我们就学习完了,下面看一个完整的例子感受一下。

完整的例子

下面是一个关于咱们上面所说的暴露的方法的使用的例子,你可以跟着我的步骤自己动手练习一下。

//创建匹配器,即如何使用查询条件
ExampleMatcher exampleMatcher = ExampleMatcher
      //采用默认and的查询方式
      .matchingAll()
      //忽略大小写
      .withIgnoreCase()
      //忽略所有null值的字段
      .withIgnoreNullValues()
      .withIgnorePaths("id","createDate")
      //默认采用精准匹配规则
      .withStringMatcher(ExampleMatcher.StringMatcher.EXACT)
      //级联查询,字段user.email采用字符前缀匹配规则
      .withMatcher("user.email", ExampleMatcher.GenericPropertyMatchers.startsWith())
      //特殊指定address字段采用后缀匹配
      .withMatcher("address", ExampleMatcher.GenericPropertyMatchers.endsWith());
Page<UserAddress> u = userAddressRepository.findAll(Example.of(address,exampleMatcher), PageRequest.of(0,2));

这时候可能会有同学问了,我是怎么知道默认值的呢?我们直接看类的构造方法就可以了,如下所示:

Drawing 6.png

从源码中我们可以看到,实现类的构造方法只有一个,就是“赋值默认”的方式。下面我整理了一些在使用这个语法时需要考虑的细节。

ExampleExceutor 使用中需要考虑的因素
  1. Null 值的处理:当某个条件值为 Null 时,是应当忽略这个过滤条件,还是应当去匹配数据库表中该字段值是 Null 的记录呢?

  2. 忽略某些属性值:一个实体对象,有许多个属性,是否每个属性都参与过滤?是否可以忽略某些属性?

  3. 不同的过滤方式:同样是作为 String 值,可能“姓名”希望精确匹配,“地址”希望模糊匹配,如何做到?

那么接下来我们分析一下源码看看其原理,说了这么半天,它到底和 JpaSpecificationExecutor 什么关系呢?我们接着看。

QueryByExampleExecutor 源码分析

怎么分析源码也很简单,我们看一下上面的我们 findAll 的方法调用之处。

Drawing 7.png

从而找到 findAll 方法的实现类,如下所示:

Drawing 8.png

通过 Debug 断点我们可以看到,我们刚才组合出来的 Example 对象,这个时候被封装成了 ExampleSpecification 对象,那么我们接着往下看方法里面的关键内容。

TypedQuery<S> query = getQuery(new ExampleSpecification<>(example, escapeCharacter), probeType, pageable);

getQuery 方法是创建 Query 的关键,因为它里面做了条件的转化逻辑。那么我们再看一下参数 ExampleSpecification 的源码,发现它是接口 Specification 的实现类,并且是非公开的实现类,可以通过接口对外暴露 and、or、not、where 等组合条件的查询条件。

Drawing 9.png

我们接着看上面的 getQuery 方法的实现,可以看到接收的参数是 Specification<S>接口,所以不用关心实现类是什么。

Drawing 10.png

我们接着再看这个断点的 getQuery 方法:

Drawing 11.png

里面有一段代码会调用 applySpecificationToCriteria 生成 root,并由 Root 作为参数生成 Query,从而交给 EM(EntityManager)进行查询。

我们再来看一下关键的 applySpecificationToCriteria 方法。

Drawing 12.png

根据 Specification 调用 toPredicate 方法,生成 Predicate,从而实现查询需求。

现在我们已经对 QueryByExampleExecutor 的用法和实现原理基本掌握了,我们再来看一个十分相似的接口:JpaSpecificationExecutor 是干什么用的。

JpaSpecificationExecutor 接口结构

正如我们开篇提到的【图一:Repository 类图】,JpaSpecificationExecutor 是 JPA 里面的另一个接口分支。我们先来看看它的基本语法。

Drawing 13.png

我们通过查看 JpaSpecificationExecutor 的 Structure 图会发现,方法就有这么几个,细心的同学这个时候会发现它的参数 Specification,正是我们分析 QueryByExampleExecutor 的原理时候使用的 Specification。

那么 JpaSpecificationExecutor 帮我们解决了哪些问题呢?

JpaSpecificationExecutor 解决了哪些问题

  1. 我们通过 QueryByExampleExecutor 的使用方法和原理分析,不难发现,JpaSpecificationExecutor 的查询条件 Specification 十分灵活,可以帮我们解决动态查询条件的问题,正如 QueryByExampleExecutor 的用法一样;

  2. 它提供的 Criteria API 的使用封装,可以用于动态生成 Query 来满足我们业务中的各种复杂场景;

  3. 既然QueryByExampleExecutor 能利用 Specification 封装成框架,我们是不是也可以利用 JpaSpecificationExecutor 封装成框架呢?这样就学会了举一反三。

总结

本课时我们通过分析QueryByExampleExecutor 的详细用法和实现原理,知道了 Specification 的应用场景,那么下一课时我会详细介绍JpaSpecificationExecutor 的用法和实现原理。

另外本课时也提供了一种学习框架的思路,就是怎么通过源码来详细掌握语法。

总之,保持一颗好奇心,不断深挖,你才能掌握得更加全面。本节课就到这里了,如果你觉得有帮助,欢迎你留言讨论和分享,我们下一课时再见。

点击下方链接查看源码(不定时更新)
https://github.com/zhangzhenhuajack/spring-boot-guide/tree/master/spring-data/spring-data-jpa


10 JpaSpecificationExecutor 实现的原理是什么?

通过上一课时,我们了解到 JpaSpecificationExecutor 给我们提供了动态查询或者写框架的一种思路,那么这节课我们来看一下 JpaSpecificationExecutor 的详细用法和原理,及其实战应用场景中如何实现自己的框架。

在开始讲解之前,请先思考几个问题:

  1. JpaSpecificationExecutor 如何创建?

  2. 它的使用方法有哪些?

  3. toPredicate 方法如何实现?

带着这些问题,我们开始探索。先看一个例子感受一下 JpaSpecificationExecutor 具体的用法。

JpaSpecificationExecutor 使用案例

我们假设一个后台管理页面根据 name 模糊查询、sex 精准查询、age 范围查询、时间区间查询、address 的 in 查询这样一个场景,来查询 user 信息,我们看看这个例子应该怎么写。

第一步:创建 User 和 UserAddress 两个实体。 代码如下:

package com.example.jpa.example1;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.*;
import javax.persistence.*;
import java.io.Serializable;
import java.time.Instant;
import java.util.Date;
import java.util.List;
/**
* 用户基本信息表
**/
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "addresses")
public class User implements Serializable {
   @Id
   @GeneratedValue(strategy= GenerationType.AUTO)
   private Long id;
   private String name;
   private String email;
   @Enumerated(EnumType.STRING)
   private SexEnum sex;
   private Integer age;
   private Instant createDate;
   private Date updateDate;
   @OneToMany(mappedBy = "user")
   @JsonIgnore
   private List<UserAddress> addresses;
}
enum SexEnum {
   BOY,GIRL
}
package com.example.jpa.example1;
import lombok.*;
import javax.persistence.*;
/**
 * 用户地址表
 */
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "user")
public class UserAddress {
   @Id
   @GeneratedValue(strategy= GenerationType.AUTO)
   private Long id;
   private String address;
   @ManyToOne(cascade = CascadeType.ALL)
   private User user;
}

第二步:创建 UserRepository 继承 JpaSpecificationExecutor 接口。

package com.example.jpa.example1;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface UserRepository extends JpaRepository<User,Long>, JpaSpecificationExecutor<User> {
}

第三步:创建一个测试用例进行测试。

@DataJpaTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class UserJpeTest {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private UserAddressRepository userAddressRepository;
    private Date now = new Date();
    /**
     * 提前创建一些数据
     */
    @BeforeAll
    @Rollback(false)
    @Transactional
    void init() {
        User user = User.builder()
                .name("jack")
                .email("123456@126.com")
                .sex(SexEnum.BOY)
                .age(20)
                .createDate(Instant.now())
                .updateDate(now)
                .build();
        userAddressRepository.saveAll(Lists.newArrayList(UserAddress.builder().user(user).address("shanghai").build(),
                UserAddress.builder().user(user).address("beijing").build()));
    }
    @Test
    public void testSPE() {
        //模拟请求参数
        User userQuery = User.builder()
                .name("jack")
                .email("123456@126.com")
                .sex(SexEnum.BOY)
                .age(20)
                .addresses(Lists.newArrayList(UserAddress.builder().address("shanghai").build()))
                .build();
                //假设的时间范围参数
        Instant beginCreateDate = Instant.now().plus(-2, ChronoUnit.HOURS);
        Instant endCreateDate = Instant.now().plus(1, ChronoUnit.HOURS);
        //利用Specification进行查询
        Page<User> users = userRepository.findAll(new Specification<User>() {
            @Override
            public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                List<Predicate> ps = new ArrayList<Predicate>();
                if (StringUtils.isNotBlank(userQuery.getName())) {
                    //我们模仿一下like查询,根据name模糊查询
                    ps.add(cb.like(root.get("name"),"%" +userQuery.getName()+"%"));
                }
                if (userQuery.getSex()!=null){
                    //equal查询条件,这里需要注意,直接传递的是枚举
                    ps.add(cb.equal(root.get("sex"),userQuery.getSex()));
                }
                if (userQuery.getAge()!=null){
                    //greaterThan大于等于查询条件
                    ps.add(cb.greaterThan(root.get("age"),userQuery.getAge()));
                }
                if (beginCreateDate!=null&&endCreateDate!=null){
                    //根据时间区间去查询创建
                    ps.add(cb.between(root.get("createDate"),beginCreateDate,endCreateDate));
                }
                if (!ObjectUtils.isEmpty(userQuery.getAddresses())) {
                    //联表查询,利用root的join方法,根据关联关系表里面的字段进行查询。
                    ps.add(cb.in(root.join("addresses").get("address")).value(userQuery.getAddresses().stream().map(a->a.getAddress()).collect(Collectors.toList())));
                }
                return query.where(ps.toArray(new Predicate[ps.size()])).getRestriction();
            }
        }, PageRequest.of(0, 2));
        System.out.println(users);
    }
}

我们看一下执行结果。

Hibernate: select user0_.id as id1_1_, user0_.age as age2_1_, user0_.create_date as create_d3_1_, user0_.email as email4_1_, user0_.name as name5_1_, user0_.sex as sex6_1_, user0_.update_date as update_d7_1_ from user user0_ inner join user_address addresses1_ on user0_.id=addresses1_.user_id where (user0_.name like ?) and user0_.sex=? and user0_.age>20 and (user0_.create_date between ? and ?) and (addresses1_.address in (?)) limit ?

此 SQL 的参数如下:

Drawing 0.png

此 SQL 就是查询 User inner Join user_address 之后组合成的查询 SQL,基本符合我们的预期,即不同的查询条件。我们通过这个例子大概知道了 JpaSpecificationExecutor 的用法,那么它具体是什么呢?

JpaSpecificationExecutor 语法详解

我们依然通过看 JpaSpecificationExecutor 的源码,来了解一下它的几个使用方法,如下所示:

public interface JpaSpecificationExecutor<T> {
   //根据 Specification 条件查询单个对象,要注意的是,如果条件能查出来多个会报错
   T findOne(@Nullable Specification<T> spec);
   //根据 Specification 条件,查询 List 结果
   List<T> findAll(@Nullable Specification<T> spec);
   //根据 Specification 条件,分页查询
   Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);
   //根据 Specification 条件,带排序的查询结果
   List<T> findAll(@Nullable Specification<T> spec, Sort sort);
   //根据 Specification 条件,查询数量
   long count(@Nullable Specification<T> spec);
}

其返回结果和 Pageable、Sort,我们在前面课时都有介绍过,这里我们重点关注一下 Specification。看一下 Specification 接口的代码。

Drawing 1.png

通过看其源码就会发现里面提供的方法很简单。其中,下面一段代码表示组合的 and 关系的查询条件。

default Specification<T> and(@Nullable Specification<T> other) {
   return composed(this, other, (builder, left, rhs) -> builder.and(left, rhs));
}

下面是静态方法,创建 where 后面的 Predicate 集合。

static <T> Specification<T> where(@Nullable Specification<T> spec)

下面是默认方法,创建 or 条件的查询参数。

default Specification<T> or(@Nullable Specification<T> other)

这是静态方法,创建 Not 的查询条件。

static <T> Specification<T> not(@Nullable Specification<T> spec)

上面这几个方法比较简单,我就不一一细说了,我们主要看一下需要实现的方法:toPredicate。

Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);

toPredicate 这个方法是我们用到的时候需要自己去实现的,接下来我们详细介绍一下。

首先我们在刚才的 Demo 里面设置一个断点,看到如下界面。

Drawing 2.png

这里可以分别看到 Root 的实现类是 RootImpl,CriteriaQuery 的实现类是 CriteriaQueryImpl,CriteriaBuilder 的实现类是 CriteriaBuilderImpl。

javax.persistence.criteria.Root
javax.persistence.criteria.CriteriaQuery
javax.persistence.criteria.CriteriaBuilder

其中,上面三个接口是 Java Persistence API 定义的接口。

org.hibernate.query.criteria.internal.path.RootImpl
rg.hibernate.query.criteria.internal.CriteriaQueryImpl
org.hibernate.query.criteria.internal.CriteriaBuilderImpl

而这个三个实现类都是由 Hibernate 进行实现的,也就是说 JpaSpecificationExecutor 封装了原本需要我们直接操作 Hibernate 中 Criteria 的 API 方法。

下面分别解释上述三个参数。

Root<User> root

代表了可以查询和操作的实体对象的根,如果将实体对象比喻成表名,那 root 里面就是这张表里面的字段,而这些字段只是 JPQL 的实体字段而已。我们可以通过里面的 Path get(String attributeName),来获得我们想要操作的字段。

类似于我们上面的:root.get("createDate")等操作
CriteriaQuery<?> query

代表一个 specific 的顶层查询对象,它包含着查询的各个部分,比如 select 、from、where、group by、order by 等。CriteriaQuery 对象只对实体类型或嵌入式类型的 Criteria 查询起作用。简单理解为,它提供了查询 ROOT 的方法。常用的方法有如下几种:

Drawing 3.png

正如我们上面where的用法:query.where(.....)一样

这个语法比较简单,我们在其方法后面加上相应的参数即可。下面看一个 group by 的例子。

Drawing 4.png

如上图所示,我们加入 groupyBy 某个字段,SQL 也会有相应的变化。那么我们再来看第三个参数。

CriteriaBuilder cb

CriteriaBuilder 是用来构建 CritiaQuery 的构建器对象,其实就相当于条件或者条件组合,并以 Predicate 的形式返回。它基本上提供了所有常用的方法,如下所示:

Drawing 5.png

我们直接通过此类的 Structure 视图就可以看到都有哪些方法。其中,and、any 等用来做查询条件的组合;类似 between、equal、exist、ge、gt、isEmpty、isTrue、in 等用来做查询条件的查询,类似下图的一些方法。

Drawing 6.png

而其中 Expression 很简单,都是通过 root.get(...) 某些字段即可返回,正如下面的用法。

Predicate p1=cb.like(root.get(“name”).as(String.class), “%”+uqm.getName()+“%”);
Predicate p2=cb.equal(root.get("uuid").as(Integer.class), uqm.getUuid());
Predicate p3=cb.gt(root.get("age").as(Integer.class), uqm.getAge());

我们利用 like、equal、gt 可以生产 Predicate,而 Predicate 可以组合查询。比如我们预定它们之间是 and 或 or 的关系:Predicate p = cb.and(p3,cb.or(p1,p2));

我们让 p1 和 p2 是 or 的关系,并且得到的 Predicate 和 p3 又构成了 and 的关系。你可以发现它的用法还是比较简单的,正如我们开篇所说的 Junit 中 test 里面一样的写法。

关于 JpaSpecificationExecutor 的语法我们就介绍完了,其实它里面的功能相当强大,只是我们发现 Spring Data JPA 介绍得并不详细,只是一笔带过,可能他们认为写框架的人才能用到,所以介绍得不多。

如果你想了解更多语法的话,可以参考 Hibernate 的文档:https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#criteria。我们再来看看 JpaSpecificationExecutor 的实现原理。

JpaSpecificationExecutor 原理分析

我们先看一下 JpaSpecificationExecutor 的类图。

Drawing 7.png

从图中我们可以看得出来:

  1. JpaSpecificationExecutor 和 JpaRepository 是平级接口,而它们对应的实现类都是 SimpleJpaRepository;

  2. Specification 被 ExampleSpecification 和 JpaSpecificationExector 使用,用来创建查询;

  3. Predicate 是 JPA 协议里面提供的查询条件的根基;

  4. SimpleJpaRepository 利用 EntityManager 和 criteria 来实现由 JpaSpecificationExector 组合的 query。

那么我们再直观地看一下 JpaSpecificationExecutor 接口里面的方法 findAll 对应的 SimpleJpaRepository 里面的实现方法 findAl,我们通过工具可以很容易地看到相应的实现方法,如下所示:

Drawing 8.png

你要知道,得到 TypeQuery 就可以直接操作JPA协议里面相应的方法了,那么我们看下 getQuery(spec,pageable)的实现过程。

Drawing 9.png

之后一步一步 debug 就可以了。

Drawing 10.png

到了上图所示这里,就可以看到:

  1. Specification<S> spec 是我们测试用例写的 specification 的匿名实现类;

  2. 由于是方法传递,所以到第 693 行断点的时候,才会执行我们在测试用里面写的 Specification;

  3. 我们可以看到这个方法最后是调用的 EntityManager,而 EntitytManger 是 JPA 操作实体的核心原理,我在下一课时讲自定义 Repsitory 的时候再详细介绍;

  4. 从上面的方法实现过程中我们可以看得出来,所谓的JpaSpecificationExecutor原理,用一句话概况,就是利用Java Persistence API定义的接口和Hibernate的实现,做了一个简单的封装,方便我们操作JPA协议中 criteria 的相关方法。

到这里,原理和使用方法,我们基本介绍完了。你可能会有疑问:这个感觉有点重要,但是一般用不到吧?那么接下来我们看看 JpaSpecificationExecutor 的实战应用场景是什么样的。

JpaSpecificationExecutor 实战应用场景

其实JpaSpecificationExecutor 的目的不是让我们做日常的业务查询,而是给我们提供了一种自定义 Query for rest 的架构思路,如果做日常的增删改查,肯定不如我们前面介绍的 Defining Query Methods 和 @Query 方便。

那么来看下,实战过程中如何利用 JpaSpecificationExecutor 写一个框架。

MySpecification 自定义

我们可以自定义一个Specification 的实现类,它可以实现任何实体的动态查询和各种条件的组合。

package com.example.jpa.example1.spe;
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.*;
public class MySpecification<Entity> implements Specification<Entity> {
   private SearchCriteria criteria;
   public MySpecification (SearchCriteria criteria) {
      this.criteria = criteria;
   }
   /**
    * 实现实体根据不同的字段、不同的Operator组合成不同的Predicate条件
    *
    * @param root            must not be {@literal null}.
    * @param query           must not be {@literal null}.
    * @param builder  must not be {@literal null}.
    * @return a {@link Predicate}, may be {@literal null}.
    */
   @Override
   public Predicate toPredicate(Root<Entity> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
      if (criteria.getOperation().compareTo(Operator.GT)==0) {
         return builder.greaterThanOrEqualTo(
               root.<String> get(criteria.getKey()), criteria.getValue().toString());
      }
      else if (criteria.getOperation().compareTo(Operator.LT)==0) {
         return builder.lessThanOrEqualTo(
               root.<String> get(criteria.getKey()), criteria.getValue().toString());
      }
      else if (criteria.getOperation().compareTo(Operator.LK)==0) {
         if (root.get(criteria.getKey()).getJavaType() == String.class) {
            return builder.like(
                  root.<String>get(criteria.getKey()), "%" + criteria.getValue() + "%");
         } else {
            return builder.equal(root.get(criteria.getKey()), criteria.getValue());
         }
      }
      return null;
   }
}

我们通过 <Entity> 泛型,解决不同实体的动态查询(当然了,我只是举个例子,这个方法可以进行无限扩展)。我们通过 SearchCriteria 可以知道不同的字段是什么、值是什么、如何操作的,看一下代码:

package com.example.jpa.example1.spe;
import lombok.*;
/**
 * @author jack,实现不同的查询条件,不同的操作,针对Value;
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SearchCriteria {
   private String key;
   private Operator operation;
   private Object value;
}

其中的 Operator 也是我们自定义的。

package com.example.jpa.example1.spe;
public enum Operator {
   /**
    * 等于
    */
   EQ("="),
   /**
    * 等于
    */
   LK(":"),
   /**
    * 不等于
    */
   NE("!="),
   /**
    * 大于
    */
   GT(">"),
   /**
    * 小于
    */
   LT("<"),
   /**
    * 大于等于
    */
   GE(">=");
   Operator(String operator) {
      this.operator = operator;
   }
   private String operator;
}

在 Operator 枚举里面定义了逻辑操作符(大于、小于、不等于、等于、大于等于……也可以自己扩展),并在 MySpecification 里面进行实现。那么我们来看看它是怎么用的,写一个测试用例试一下。

/**
 * 测试自定义的Specification语法
 */
@Test
public void givenLast_whenGettingListOfUsers_thenCorrect() {
    MySpecification<User> name =
        new MySpecification<User>(new SearchCriteria("name", Operator.LK, "jack"));
MySpecification<User> age =
        new MySpecification<User>(new SearchCriteria("age", Operator.GT, 2));
List<User> results = userRepository.findAll(Specification.where(name).and(age));
    System.out.println(results.get(0).getName());
}

你就会发现,我们在调用findAll 组合 Predicate 的时候就会非常简单,省去了各种条件的判断和组合,而省去的这些逻辑可以全部在我们的框架代码 MySpecification 里面实现。

那么如果把这个扩展到 API 接口层面会是什么样的结果呢?我们来看下。

利用 Specification 创建 search 为查询条件的 Rest API 接口

先创建一个 Controller,用来接收 search 这样的查询条件:类似 userssearch=lastName:doe,age>25 的参数。

package com.example.jpa.example1.web;
import com.example.jpa.example1.User;
import com.example.jpa.example1.UserRepository;
import com.example.jpa.example1.spe.SpecificationsBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
public class UserController {
    @Autowired
    private UserRepository repo;
    @RequestMapping(method = RequestMethod.GET, value = "/users")
    @ResponseBody
    public List<User> search(@RequestParam(value = "search") String search) {
        Specification<User> spec = new SpecificationsBuilder<User>().buildSpecification(search);
        return repo.findAll(spec);
    }
}

Controller 里面非常简单,利用 SpecificationsBuilder 生成我们需要的 Specification 即可。那么我们看看 SpecificationsBuilder 里面是怎么写的。

package com.example.jpa.example1.spe;
import com.example.jpa.example1.User;
import org.springframework.data.jpa.domain.Specification;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
 * 处理请求参数
 * @param <Entity>
 */
public class SpecificationsBuilder<Entity> {
   private final List<SearchCriteria> params;

//初始化params,保证每次实例都是一个新的ArrayList
public SpecificationsBuilder() {
params = new ArrayList<SearchCriteria>();
}

//利用正则表达式取我们search参数里面的值,解析成SearchCriteria对象
public Specification<Entity> buildSpecification(String search) {
Pattern pattern = Pattern.compile(“(\w+?)(:|<|>)(\w+?),”);
Matcher matcher = pattern.matcher(search + “,”);
while (matcher.find()) {
this.with(matcher.group(1), Operator.fromOperator(matcher.group(2)), matcher.group(3));
}
return this.build();
}
//根据参数返回我们刚才创建的SearchCriteria
private SpecificationsBuilder with(String key, Operator operation, Object value) {
params.add(new SearchCriteria(key, operation, value));
return this;
}
//根据我们刚才创建的MySpecification返回所需要的Specification
private Specification<Entity> build() {
if (params.size() == 0) {
return null;
}
List<Specification> specs = params.stream()
.map(MySpecification<User>::new)
.collect(Collectors.toList());
Specification result = specs.get(0);
for (int i = 1; i < params.size(); i++) {
result = Specification.where(result)
.and(specs.get(i));
}
return result;
}
}

通过上面的代码,我们可以看到通过自定义的 SpecificationsBuilder,来处理请求参数 search 里面的值,然后转化成我们上面写的 SearchCriteria 对象,再调用 MySpecification 生成我们需要的 Specification,从而利用 JpaSpecificationExecutor 实现查询效果。是不是学起来并不困难了,你学会了吗?

总结

本课时,我们通过实例学习了 JpaSpecificationExecutor 的用法,并且通过源码了解了 JpaSpecificationExecutor 的实现原理,最后我举了一个实战场景的例子,使我们可以利用 Spring Data JPA and Specifications 很轻松地创建一个基于 Search 的 Rest API。虽然我介绍的这个例子还有很多可以扩展的地方,但是希望你根据实际情况再进行相应的扩展。

这里顺带留一个思考题:怎么查询 UserAddress?提示你可以利用我上面提到的 SpecificationsBuilder 进行解决。

本课时到这就结束了,欢迎你在下面留言讨论或分享。下节课,我会为你讲解如何自定义 Repository。

点击下方链接查看源码(不定时更新)
https://github.com/zhangzhenhuajack/spring-boot-guide/tree/master/spring-data/spring-data-jpa


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

办公模板库 素材蛙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值