Spring Data JPA 之 QueryByExampleExecutor 的用法和原理分析

Spring Data JPA 之 QueryByExampleExecutor 的用法和原理分析

9.1 QueryByExampleExecutor 的用法

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

下⾯是⼀个 UML 图,你可以看到 QueryByExampleExecutor 是 JpaRepository 的⽗接⼝,也就是 JpaRespository ⾥⾯继承了 QueryByExampleExecutor 的所有⽅法。

在这里插入图片描述

9.1.1 基本方法
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 写⼀个分⻚查询的例⼦,看⼀下效果。

9.1.2 使用案例

我们还⽤先前的 User 实体和 UserAddress 实体,并把 User 变丰富⼀点,这样⽅便测试。

两个实体关键代码如下。

// 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;
}
public enum SexEnum {
    BOY, GIRL
}
//UserAddress基本上不变
@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;
}

可以看出两个实体我们加了些字段。UserAddressRepository 继承 JpaRepository,从⽽也继承了 QueryByExampleExceutor ⾥⾯的⽅法,如下所示。

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

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

@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_5_, useraddres0_.address as address2_5_, useraddres0_.user_id as user_id3_5_ from user_address useraddres0_ inner join user user1_ on useraddres0_.user_id=user1_.id where (useraddres0_.address like ? escape ?) and (user1_.email like ? escape ?) and user1_.age=20 and user1_.name=? limit ?

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

{
    "content" : [ {
        "id" : 1,
        "address" : "shanghai"
    } ],
    "pageable" : {
        "sort" : {
            "sorted" : false,
            "unsorted" : true,
            "empty" : true
        },
        "pageNumber" : 0,
        "pageSize" : 2,
        "offset" : 0,
        "paged" : true,
        "unpaged" : false
    },
    "last" : true,
    "totalPages" : 1,
    "totalElements" : 1,
    "first" : true,
    "numberOfElements" : 1,
    "size" : 2,
    "number" : 0,
    "sort" : {
        "sorted" : false,
        "unsorted" : true,
        "empty" : true
    },
    "empty" : false
}

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

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

9.2 QueryByExampleExecutor 的语法

9.2.1 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,我们分析⼀下它的语法。

9.2.2 ExampleMatcher 方法概述

我们通过分析 ExampleMatcher 的源码来分析⼀下其⽤法。

⾸先打开 Structure 视图,看看⾥⾯对外暴露的⽅法都有哪些。

在这里插入图片描述

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

TypedExampleMatcher 不是 public 类型的,所以我们可以基本上不⽤看了,主要看⼀下接⼝⾥⾯给我们暴露了哪些实例化⽅法。

9.2.3 初始化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 语法给我们暴露的⽅法有哪些。

9.2.4 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_5_, useraddres0_.address as address2_5_, useraddres0_.user_id as user_id3_5_ from user_address useraddres0_ inner join user user1_ on useraddres0_.user_id=user1_.id where (useraddres0_.id is null) and (user1_.update_date is null) and (user1_.create_date is null) and user1_.name=? and (user1_.email like ? escape ?) and (user1_.id is null) and (user1_.sex is null) and user1_.age=20 and (useraddres0_.address like ? escape ?) limit ?

这样就会导致我们⼀条数据都查不出来了。

忽略某些 Paths,不参加查询条件

// 忽略某些属性列表,不参与查询过滤条件。
ExampleMatcher withIgnorePaths(String... ignoredPaths);

字符串字段默认的匹配规则

ExampleMatcher withStringMatcher(StringMatcher defaultStringMatcher);

关于默认字符串的匹配⽅式,枚举类型有 6 个可选值,DEFAULT(默认,效果同 EXACT)、EXACT(相等)、STARTING(开始匹配)、ENDING(结束匹配)、CONTAINING(包含,模糊匹配)、REGEX(正则表达式)。

字符串匹配规则,我们和 JPQL 对应到⼀起举例,如下表所示:

字符串匹配方式对应 JPQL 的写法
DEFAULT & 不忽略大小写firstname=?1
EXACT & 忽略大小写LOWER(firstname) = LOWER(?1)
STARTING & 忽略大小写LOWER(firstname) like LOWER(?1)+‘%’
ENDING & 不忽略大小写firstname like ‘%’+?1
CONTAINING & 不忽略大小写Firstname like ‘%’+?1+‘%’

相关代码如下:

ExampleMatcher withMatcher(String propertyPath, GenericPropertyMatcher genericPropertyMatcher);

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

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

在这里插入图片描述

到这⾥,语法部分我们就学习完了,下⾯看⼀个完整的例⼦感受⼀下。

9.2.5 ExampleMatcher 的完整例子

下⾯是⼀个上⾯所说的暴露的⽅法的使⽤的例⼦

// 创建匹配器,即如何使⽤查询条件
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));

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

在这里插入图片描述

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

9.2.6 使用 QueryByExampleExecutor 时需要考虑的因素
  1. Null 值的处理:当某个条件值为 Null 时,是应当忽略这个过滤条件,还是应当去匹配数据库表中该字段值是 Null 的记录呢?
  2. 忽略某些属性值:⼀个实体对象,有许多个属性,是否每个属性都参与过滤?是否可以忽略某些属性?
  3. 不同的过滤⽅式:同样是作为 String 值,可能“姓名”希望精确匹配,“地址”希望模糊匹配,如何做到?

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

9.3 QueryByExampleExecutor 的实现原理

9.3.1 QueryByExampleExecutor 的源码分析

怎么分析源码也很简单,我们看⼀下上⾯的我们 findAll 的⽅法调⽤之处。

Page<UserAddress> u = userAddressRepository.findAll(Example.of(address, exampleMatcher), PageRequest.of(0, 2));

从⽽找到 findAll ⽅法的实现类 SImpleJpaRepository,如下所示:

在这里插入图片描述

通过 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 ⽅法。org.springframework.data.jpa.repository.support.SimpleJpaRepository#applySpecificationToCriteria

在这里插入图片描述

根据 Specification 调⽤ toPredicate ⽅法,⽣成 Predicate,从⽽实现查询需求。

现在我们已经对 QueryByExampleExecutor 的⽤法和实现原理基本掌握了,我们再来看⼀个⼗分相似的接⼝:JpaSpecificationExecutor 是⼲什么⽤的。

9.3.2 JpaSpecificationExecutor 的接口结构

正如我们开篇提到的,JpaSpecificationExecutor 是 JPA ⾥⾯的另⼀个接⼝分⽀。我们先来看看它的基本语法。

在这里插入图片描述

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

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

  1. 我们通过 QueryByExampleExecutor 的使⽤⽅法和原理分析,不难发现,JpaSpecificationExecutor 的查询条件 Specification ⼗分灵活,可以帮我们解决动态查询条件的问题,正如 QueryByExampleExecutor 的⽤法⼀样;
  2. 它提供的 Criteria API 的使⽤封装,可以⽤于动态⽣成 Query 来满⾜我们业务中的各种复杂场景;
  3. 既然QueryByExampleExecutor 能利⽤ Specification 封装成框架,我们是不是也可以利⽤ JpaSpecificationExecutor 封装成框架呢?这样就学会了举⼀反三。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值