09 JpaSpecificationExecutor 解决了哪些问题?
欢迎来到第二个模块,从这一课时开始,我们就要进入高级用法与实战的学习。在进阶高级开发 / 架构师的路上,我将尽可能把经验都传授给你,帮助你少走弯路。
学习完前面 8 个课时,相信作为一名开发人员,你对 JPA 的基本用法已经有了一定的了解。那么从这一课时开始,我们要介绍一些复杂场景的使用,特别是作为一名架构师必须要掌握的内容。
我们先来看看除了前几节课我们讲解的 Define Query Method 和 @Query 之外,还有哪些查询方法。首先看一个简单的 QueryByExampleExecutor 用法。
QueryByExampleExecutor用法
QueryByExampleExecutor(QBE)是一种用户友好的查询技术,具有简单的接口,它允许动态查询创建,并且不需要编写包含字段名称的查询。
下面是一个 UML 图,你可以看到 QueryByExampleExecutor 是 JpaRepository 的父接口,也就是 JpaRespository 里面继承了 QueryByExampleExecutor 的所有方法。
图一: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,还挺符合我们的预期的,所以我们也能得到正确响应的查询结果,如下图:
也就是一个地址带一个 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 的话,只有两个方法:
-
static
<T>
Example<T>
of(T probe):需要一个实体参数,即查询的条件。而里面的 ExampleMatcher 采用默认的 ExampleMatcher.matching(); 表示忽略 Null,所有字段采用精准匹配。 -
static
<T>
Example<T>
of(T probe, ExampleMatcher matcher):需要两个参数构建 Example,也就表示了 ExampleMatcher 自由组合规则,正如我们上面的测试用例里面的代码一样。
那么现在又遇到个类:ExampleMatcher,我们分析一下它的语法。
ExampleMatcher 语法分析
我们通过分析 ExampleMatcher 的源码来分析一下其用法。
首先打开 Structure 视图,看看里面对外暴露的方法都有哪些。
通过 Structure 视图可以很容易地发现,我们要关心的方法都是这些 public 类型的返回 ExampleMatcher 的方法,那么我们把这些方法搞明白了是不是就可以掌握其详细用法了呢?下面看看它的实现类。
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 对应到一起举例,如下表所示:
相关代码如下:
ExampleMatcher withMatcher(String propertyPath, GenericPropertyMatcher genericPropertyMatcher);
这里显示的是指定某些属性的匹配规则,我们看一下 GenericPropertyMatcher 是什么东西,它都提供了哪些方法。
如下图,基本可以看出来都是针对字符串属性提供的匹配规则,也就是可以通过这个方法定制不同属性的 StringMatcher 规则。
到这里,语法部分我们就学习完了,下面看一个完整的例子感受一下。
完整的例子
下面是一个关于咱们上面所说的暴露的方法的使用的例子,你可以跟着我的步骤自己动手练习一下。
//创建匹配器,即如何使用查询条件
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));
这时候可能会有同学问了,我是怎么知道默认值的呢?我们直接看类的构造方法就可以了,如下所示:
从源码中我们可以看到,实现类的构造方法只有一个,就是“赋值默认”的方式。下面我整理了一些在使用这个语法时需要考虑的细节。
ExampleExceutor 使用中需要考虑的因素
-
Null 值的处理:当某个条件值为 Null 时,是应当忽略这个过滤条件,还是应当去匹配数据库表中该字段值是 Null 的记录呢?
-
忽略某些属性值:一个实体对象,有许多个属性,是否每个属性都参与过滤?是否可以忽略某些属性?
-
不同的过滤方式:同样是作为 String 值,可能“姓名”希望精确匹配,“地址”希望模糊匹配,如何做到?
那么接下来我们分析一下源码看看其原理,说了这么半天,它到底和 JpaSpecificationExecutor 什么关系呢?我们接着看。
QueryByExampleExecutor 源码分析
怎么分析源码也很简单,我们看一下上面的我们 findAll 的方法调用之处。
从而找到 findAll 方法的实现类,如下所示:
通过 Debug 断点我们可以看到,我们刚才组合出来的 Example 对象,这个时候被封装成了 ExampleSpecification 对象,那么我们接着往下看方法里面的关键内容。
TypedQuery<S> query = getQuery(new ExampleSpecification<>(example, escapeCharacter), probeType, pageable);
getQuery 方法是创建 Query 的关键,因为它里面做了条件的转化逻辑。那么我们再看一下参数 ExampleSpecification 的源码,发现它是接口 Specification 的实现类,并且是非公开的实现类,可以通过接口对外暴露 and、or、not、where 等组合条件的查询条件。
我们接着看上面的 getQuery 方法的实现,可以看到接收的参数是 Specification<S>
接口,所以不用关心实现类是什么。
我们接着再看这个断点的 getQuery 方法:
里面有一段代码会调用 applySpecificationToCriteria 生成 root,并由 Root 作为参数生成 Query,从而交给 EM(EntityManager)进行查询。
我们再来看一下关键的 applySpecificationToCriteria 方法。
根据 Specification 调用 toPredicate 方法,生成 Predicate,从而实现查询需求。
现在我们已经对 QueryByExampleExecutor 的用法和实现原理基本掌握了,我们再来看一个十分相似的接口:JpaSpecificationExecutor 是干什么用的。
JpaSpecificationExecutor 接口结构
正如我们开篇提到的【图一:Repository 类图】,JpaSpecificationExecutor 是 JPA 里面的另一个接口分支。我们先来看看它的基本语法。
我们通过查看 JpaSpecificationExecutor 的 Structure 图会发现,方法就有这么几个,细心的同学这个时候会发现它的参数 Specification,正是我们分析 QueryByExampleExecutor 的原理时候使用的 Specification。
那么 JpaSpecificationExecutor 帮我们解决了哪些问题呢?
JpaSpecificationExecutor 解决了哪些问题
-
我们通过 QueryByExampleExecutor 的使用方法和原理分析,不难发现,JpaSpecificationExecutor 的查询条件 Specification 十分灵活,可以帮我们解决动态查询条件的问题,正如 QueryByExampleExecutor 的用法一样;
-
它提供的 Criteria API 的使用封装,可以用于动态生成 Query 来满足我们业务中的各种复杂场景;
-
既然QueryByExampleExecutor 能利用 Specification 封装成框架,我们是不是也可以利用 JpaSpecificationExecutor 封装成框架呢?这样就学会了举一反三。
总结
本课时我们通过分析QueryByExampleExecutor 的详细用法和实现原理,知道了 Specification 的应用场景,那么下一课时我会详细介绍JpaSpecificationExecutor 的用法和实现原理。
另外本课时也提供了一种学习框架的思路,就是怎么通过源码来详细掌握语法。
总之,保持一颗好奇心,不断深挖,你才能掌握得更加全面。本节课就到这里了,如果你觉得有帮助,欢迎你留言讨论和分享,我们下一课时再见。
点击下方链接查看源码(不定时更新)
https://github.com/zhangzhenhuajack/spring-boot-guide/tree/master/spring-data/spring-data-jpa
10 JpaSpecificationExecutor 实现的原理是什么?
通过上一课时,我们了解到 JpaSpecificationExecutor 给我们提供了动态查询或者写框架的一种思路,那么这节课我们来看一下 JpaSpecificationExecutor 的详细用法和原理,及其实战应用场景中如何实现自己的框架。
在开始讲解之前,请先思考几个问题:
-
JpaSpecificationExecutor 如何创建?
-
它的使用方法有哪些?
-
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 的参数如下:
此 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 接口的代码。
通过看其源码就会发现里面提供的方法很简单。其中,下面一段代码表示组合的 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 里面设置一个断点,看到如下界面。
这里可以分别看到 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 的方法。常用的方法有如下几种:
正如我们上面where的用法:query.where(.....)一样
这个语法比较简单,我们在其方法后面加上相应的参数即可。下面看一个 group by 的例子。
如上图所示,我们加入 groupyBy 某个字段,SQL 也会有相应的变化。那么我们再来看第三个参数。
CriteriaBuilder cb
CriteriaBuilder 是用来构建 CritiaQuery 的构建器对象,其实就相当于条件或者条件组合,并以 Predicate 的形式返回。它基本上提供了所有常用的方法,如下所示:
我们直接通过此类的 Structure 视图就可以看到都有哪些方法。其中,and、any 等用来做查询条件的组合;类似 between、equal、exist、ge、gt、isEmpty、isTrue、in 等用来做查询条件的查询,类似下图的一些方法。
而其中 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 的类图。
从图中我们可以看得出来:
-
JpaSpecificationExecutor 和 JpaRepository 是平级接口,而它们对应的实现类都是 SimpleJpaRepository;
-
Specification 被 ExampleSpecification 和 JpaSpecificationExector 使用,用来创建查询;
-
Predicate 是 JPA 协议里面提供的查询条件的根基;
-
SimpleJpaRepository 利用 EntityManager 和 criteria 来实现由 JpaSpecificationExector 组合的 query。
那么我们再直观地看一下 JpaSpecificationExecutor 接口里面的方法 findAll 对应的 SimpleJpaRepository 里面的实现方法 findAl,我们通过工具可以很容易地看到相应的实现方法,如下所示:
你要知道,得到 TypeQuery 就可以直接操作JPA协议里面相应的方法了,那么我们看下 getQuery(spec,pageable)的实现过程。
之后一步一步 debug 就可以了。
到了上图所示这里,就可以看到:
-
Specification
<S>
spec 是我们测试用例写的 specification 的匿名实现类; -
由于是方法传递,所以到第 693 行断点的时候,才会执行我们在测试用里面写的 Specification;
-
我们可以看到这个方法最后是调用的 EntityManager,而 EntitytManger 是 JPA 操作实体的核心原理,我在下一课时讲自定义 Repsitory 的时候再详细介绍;
-
从上面的方法实现过程中我们可以看得出来,所谓的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