java高级开发

JPA 之 QueryByExampleExecutor

2023-02-08  本文已影响0人  老鼠AI大米_Java全栈

QueryByExampleExecutor 的用法

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

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


image.png

基本方法

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

使用案例

我们还⽤先前的 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 这个参数,看看它具体的语法是什么。

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,分别做如下解释:

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

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

ExampleMatcher 方法概述

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

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


image.png

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

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 属性;
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 规则。


image.png

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

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));

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


image.png

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

使用 QueryByExampleExecutor 时需要考虑的因素

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

ExampleMatcher补充

nullHandler:null值处理方式,枚举类型

defaultStringMatcher: 默认字符串匹配方式,枚举类型,有6个可选值:

对所有字符串属性有效,除非该属性在propertySpecifiers中单独定义匹配方式:

Specification复杂条件查询

在日常工作的过程中,难免会遇到条件查询,接下来就来了解一下Specification条件查询。
要使用Specification条件查询,我们需要再继承JpaSpecificationExecutor接口。

public interface PetDao extends JpaRepository<Pet,Integer>, 
                                JpaSpecificationExecutor<Pet>{}

首先我们来看一下这个接口里有哪些方法。

public interface JpaSpecificationExecutor<T> {
    Optional<T> findOne(@Nullable Specification<T> spec);

    List<T> findAll(@Nullable Specification<T> spec);

    Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);

    List<T> findAll(@Nullable Specification<T> spec, Sort sort);

    long count(@Nullable Specification<T> spec);
}

可以看到每个方法都有一个允许为空的Specification对象。接下来我们对每个方法都进行测试。
一、findOne()由方法名就可以看出是查询单条数据;我们查询一条id为1的数据且name为pp的数据。
Optional findOne(@Nullable Specification spec);

@Test
public void test09(){
    Specification<Pet> specification = new Specification<Pet>() {
        @Override
        public Predicate toPredicate(Root<Pet> root, CriteriaQuery<?> query,
            CriteriaBuilder builder) {
            return query.where(
                    builder.equal(root.get("id"),1),    //equal()相当于“=”
                    builder.equal(root.get("name"),"pp")
            ).getRestriction();
        }
    };
    Optional<Pet> petOptional = petDao.findOne(specification);
    if (petOptional.isPresent()) {
        Pet pet = petOptional.get();
        System.out.println(pet);
    }
}

new 一个Specification会重写它的toPredicate方法,里面参数的含义分别是:

二、findAll() 这个方法就是查询所有符合条件的列,返回的是一个List,或者分页查询的Page。
接下来用条件查询in,查询所有id为1,2,5的pet。这里我们直接使用lambda表达式,可以简化匿名内部类的写法。List findAll(@Nullable Specification spec);

@Test
public void test11(){
    Specification<Pet> specification = (root, query, builder) ->
            query.where(
                    builder.in(root.get("id")).value(1).value(2).value(5)
            ).getRestriction();
    List<Pet> all = petDao.findAll(specification);
    all.forEach(System.out::println);
}

结果:


image.png

条件查询like,模糊查询,查询所有包含“p”的Pet。

@Test
public void test12(){
    Specification<Pet> specification = (root, query, builder) ->
            query.where(
                    builder.like(root.get("name"),'%'+"p"+"%")
            ).getRestriction();
            
    List<Pet> all = petDao.findAll(specification);
    
    all.forEach(System.out::println);
}

结果:

image.png
以上条件不变,分页展示查询出来的数据。
Page findAll(@Nullable Specification spec, Pageable pageable);
@Test
public void test13(){
    Specification<Pet> specification = (root, query, builder) ->
            query.where(
                    builder.like(root.get("name"),'%'+"p"+"%")
            ).getRestriction();
    Pageable pageable = PageRequest.of(0,5);
    
    Page<Pet> all = petDao.findAll(specification,pageable);
    
    all.forEach(System.out::println);
    System.out.println("总数量:"+all.getTotalElements());
    System.out.println("总页码:"+all.getTotalPages());
    System.out.println("当前页:"+(all.getNumber()));
    System.out.println("页面记录数:"+(all.getSize()));
}

结果:由于页码是从0开始的,实际开发中,我们记得要把页码+1。

image.png
依旧使用上面的模糊查询,我们把查询结果按id排序(由大到小)。
List findAll(@Nullable Specification spec, Sort sort);
@Test
public void test14(){
    Specification<Pet> specification = (root, query, builder) ->
            query.where(
                    builder.like(root.get("name"),'%'+"p"+"%")
            ).getRestriction();
            
    List<Pet> all = petDao.findAll(specification, Sort.by("id").descending());
    
    all.forEach(System.out::println);
}

结果:

image.png
最后一个方法是long count(@Nullable Specification spec)它其实就是查询到的总记录数,返回的是一个long类型的数字。
@Test
public void test15(){
    Specification<Pet> specification = (root, query, builder) ->
            query.where(
                    builder.like(root.get("name"),'%'+"p"+"%")
            ).getRestriction();
    long count = petDao.count(specification);
    System.out.println(count);
}

结果:

image.png
以上都是使用query构建的顶层查询条件,其实我们也可以直接使用builder构建顶层查询条件。
使用builder构建单条件查询:
@Test
public void test16(){
    Specification<Pet> specification = (root, query, builder) ->
            builder.like(root.get("name"),'%'+"p"+"%");
    List<Pet> all = petDao.findAll(specification);
    all.forEach(System.out::println);
}

结果:

image.png
使用builder构建多条件查询:
@Test
public void test17(){
    Specification<Pet> specification = (root, query, builder) ->
            builder.and(
                    builder.equal(root.get("id"),2),
                    builder.like(root.get("name"),'%'+"p"+"%")
            );
    System.out.println(petDao.findOne(specification));
}

结果:


image.png

至于使用哪种就看实际场景和个人习惯了。

上一篇下一篇

猜你喜欢

热点阅读