QueryByExampleExecutor 的使用
按示例查询(QBE)是一种用户友好的查询技术,具有简单的接口,它允许动态查询创建,并且不需要编写包含字段名称的查询。从 UML 图中,可以看出继承 JpaRepository 接口后,自动拥有了按“实例”进行查询的诸多方法,可见 Spring Data 的团队已经认为了 QBE 是 Spring JPA 的基本功能了,继承 QueryByExampleExecutor 和继承 JpaRepository 都会有这些基本方法,所以 QueryByExampleExecutor 位于 Spring Data Common 中,而 JpaRepository 位于 Spring Data JPA 中。
QueryByExampleExecutor 详细配置
先来看一下 QueryByExampleExecutor 的源码:
public interface QueryByExampleExecutor {
//根据“实例”查找一个对象。
S findOne(Example example);
//根据“实例”查找一批对象
Iterable findAll(Example example);
//根据“实例”查找一批对象,且排序
Iterable findAll(Example example, Sort sort);
//根据“实例”查找一批对象,且排序和分页
Page findAll(Example example, Pageable pageable);
//根据“实例”查找,返回符合条件的对象个数
long count(Example example);
//根据“实例”判断是否有符合条件的对象
boolean exists(Example example);
}
从源码上可以看出,只要了解 Example 基本上就可以掌握它的用法和 API 了。
public class Example {
@NonNull
private final T probe;
@NonNull
private final ExampleMatcher matcher;
public static Example of(T probe) {
return new Example(probe, ExampleMatcher.matching());
}
public static Example of(T probe, ExampleMatcher matcher) {
return new Example(probe, matcher);
}
......
}
我们从源码中可以看出 Example 主要包含三部分内容。
QueryByExampleExecutor 的使用案例
//创建查询条件数据对象
Customer customer = new Customer();
customer.setName("Jack");
customer.setAddress("上海");
//创建匹配器,即如何使用查询条件
ExampleMatcher matcher = ExampleMatcher.matching() //构建对象
.withMatcher("name", GenericPropertyMatchers.startsWith()) //姓名采用“开始匹配”的方式查询
.withIgnorePaths("focus"); //忽略属性:是否关注。因为是基本类型,需要忽略掉
//创建实例
Example ex = Example.of(customer, matcher);
//查询
List ls = dao.findAll(ex);
//输出结果
for (Customer bo:ls) {
System.out.println(bo.getName());
}
上面例子中,是这样创建“实例”的:
Example
可以看到,Example 对象由 customer 和 matcher 共同创建,为讲解方便,再来结合案例先来明确一些定义。
再来理解“实例查询”,顾名思义,就是通过一个例子来查询,要查询的是 Customer 对象,查询条件也是一个 Customer 对象,通过一个现有的客户对象作为例子,查询和这个例子相匹配的对象。
QueryByExampleExecutor 的特点及约束
ExampleMatcher 源码解读
(1)源码解读
public class ExampleMatcher {
NullHandler nullHandler;
StringMatcher defaultStringMatcher; //默认
boolean defaultIgnoreCase; //默认大小写忽略方式
PropertySpecifiers propertySpecifiers; //各属性特定查询方式
Set ignoredPaths; //忽略属性列表
//Null值处理方式,通过构造方法,我们发现默认忽略
NullHandler nullHandler;
//字符串匹配方式,通过构造方法可以看出默认是DEFAULT(默认,效果同EXACT),EXACT(相等)
StringMatcher defaultStringMatcher;
//各属性特定查询方式,默认无特殊指定的。
PropertySpecifiers propertySpecifiers;
//忽略属性列表,默认无。
Set ignoredPaths;
//大小写忽略方式,默认不忽略。
boolean defaultIgnoreCase;
@Wither(AccessLevel.PRIVATE) MatchMode mode;
//通用、内部、默认构造方法。
private ExampleMatcher() {
this(NullHandler.IGNORE, StringMatcher.DEFAULT, new PropertySpecifiers(), Collections.emptySet(), false,
MatchMode.ALL);
}
//Example的默认匹配方式
public static ExampleMatcher matching() {
return matchingAll();
}
public static ExampleMatcher matchingAll() {
return new ExampleMatcher().withMode(MatchMode.ALL);
}
......
}
(2)关键属性分析
(3)字符串匹配举例
字符串匹配方式 |
对应JPQL的写法 |
Default&不忽略大小写 |
firstname=?1 |
Exact&忽略大小写 |
LOWER(firstname) = LOWER(?1) |
Starting&忽略大小写 |
LOWER(firstname) like LOWER(?0)+'%' |
Ending&不忽略大小写 |
firstname like '%'+?1 |
Containing不忽略大小写 |
firstname like '%'+?1+'%' |
QueryByExampleExecutor 使用场景 & 实际的使用
使用场景
使用一组静态或动态约束来查询数据存储、频繁重构域对象,而不用担心破坏现有查询、简单的查询的使用场景,有时候还是挺方便的。
实际使用中我们需要考虑的因素
查询条件的表示,有两部分,一是条件值,二是查询方式。条件值用实体对象(如 Customer 对象)来存储,相对简单,当页面传入过滤条件值时,存入相对应的属性中,没入传入时,属性保持默认值。查询方式是用匹配器 ExampleMatcher 来表示,情况相对复杂些,需要考虑的因素有以下几个:
(1)Null 值的处理
当某个条件值为 Null时,是应当忽略这个过滤条件呢,还是应当去匹配数据库表中该字段值是 Null 的记录?
Null 值处理方式:默认值是 IGNORE(忽略),即当条件值为 Null 时,则忽略此过滤条件,一般业务也是采用这种方式就可满足。当需要查询数据库表中属性为 Null 的记录时,可将值设为 INCLUDE,这时,对于不需要参与查询的属性,都必须添加到忽略列表(ignoredPaths)中,否则会出现查不到数据的情况。
(2)基本类型的处理
如客户 Customer 对象中的年龄 age 是 int 型的,当页面不传入条件值时,它默认是0,是有值的,那是否参与查询呢?
关于基本数据类型处理方式:实体对象中,避免使用基本数据类型,采用包装器类型。如果已经采用了基本类型,而这个属性查询时不需要进行过滤,则把它添加到忽略列表(ignoredPaths)中。
(3)忽略某些属性值
一个实体对象,有许多个属性,是否每个属性都参与过滤?是否可以忽略某些属性?
ignoredPaths:虽然某些字段里面有值或者设置了其他匹配规则,只要放在 ignoredPaths 中,就会忽略此字段的,不作为过滤条件。
(4)不同的过滤方式
同样是作为 String 值,可能“姓名”希望精确匹配,“地址”希望模糊匹配,如何做到?
默认配置和特殊配置混合使用:默认创建匹配器时,字符串采用的是精确匹配、不忽略大小写,可以通过操作方法改变这种默认匹配,以满足大多数查询条件的需要,如将“字符串匹配方式”改为 CONTAINING(包含,模糊匹配),这是比较常用的情况。对于个别属性需要特定的查询方式,可以通过配置“属性特定查询方式”来满足要求,设置 propertySpecifiers 的值即可。
(5)大小写匹配
字符串匹配时,有时可能希望忽略大小写,有时则不忽略,如何做到?
defaultIgnoreCase:忽略大小的生效与否,是依赖于数据库的。例如 MySQL 数据库中,默认创建表结构时,字段是已经忽略大小写的,所以这个配置与否,都是忽略的。如果业务需要严格区分大小写,可以改变数据库表结构属性来实现。
实际使用案例说明
(1)无匹配器的情况
//创建查询条件数据对象
Customer customer = new Customer();
customer.setAddress("河南省郑州市");
customer.setFocus(true);
//创建实例
Example ex = Example.of(customer);
//查询
List ls = dao.findAll(ex);
(2)多种条件组合
//创建查询条件数据对象
Customer customer = new Customer();
customer.setName("zhang");
customer.setAddress("河南省");
customer.setRemark("BB");
//虽然有值,但是不参与过滤条件
customer.setFocus(true);
//创建匹配器,即如何使用查询条件
ExampleMatcher matcher = ExampleMatcher.matching() //构建对象
.withStringMatcher(StringMatcher.CONTAINING) //改变默认字符串匹配方式:模糊查询
.withIgnoreCase(true) //改变默认大小写忽略方式:忽略大小写
.withMatcher("address", GenericPropertyMatchers.startsWith()) //地址采用“开始匹配”的方式查询
.withIgnorePaths("focus"); //忽略属性:是否关注。因为是基本类型,需要忽略掉
//创建实例
Example ex = Example.of(customer, matcher);
//查询
List ls = dao.findAll(ex);
(3)多级查询
//创建查询条件数据对象
CustomerType type = new CustomerType();
type.setCode("01"); //编号01代表潜在客户
Customer customer = new Customer();
customer.setCustomerType(type);
//创建匹配器,即如何使用查询条件
ExampleMatcher matcher = ExampleMatcher.matching() //构建对象
.withIgnorePaths("focus"); //忽略属性:是否关注。因为是基本类型,需要忽略掉
//创建实例
Example ex = Example.of(customer, matcher);
//查询
List ls = dao.findAll(ex);
(4)查询 Null 值
//创建查询条件数据对象
Customer customer = new Customer();
//创建匹配器,即如何使用查询条件
ExampleMatcher matcher = ExampleMatcher.matching() //构建对象
//改变“Null值处理方式”:包括。
.withIncludeNullValues()
//忽略其他属性
.withIgnorePaths("id", "name", "sex", "age", "focus", "addTime", "remark", "customerType");
//创建实例
Example ex = Example.of(customer, matcher);
//查询
List ls = dao.findAll(ex);
(5)虽然我们工作中用的最多的还是“简单查询”(因为简单,所以…)和基于 JPA Criteria 的动态查询(可以满足所有需求,没有局限性)。
但是 QueryByExampleExecutor 还是个非常不错的两种中间场景的查询处理手段,其他人没有用,感觉是对其不熟悉,还是希望我们学习过 QueryByExampleExecutor 的开发者用起来,用熟悉了会增加开发效率。
QueryByExampleExecutor 的实现源码
(1)我们通过开发工具——Hierarchy,来看一下其接口的实现类有哪些:
(2)我们发现 JpaSpecificationExecutor 的实现类是 SimpleJpaRepository。
而 SimpleJpaRepository 也实现了 JpaSpecificationExecutor,于是就利用 Specification 的特性,创建了内部类 ExampleSpecification,通过 Exmaple 实现了一套工具类和对 Predicate 的构建,进而实现了整个 ExampleQuery 的逻辑。
如果我们自己去看源码,会发现 QueryByExampleExecutor 给我们提供了两种思路:
(3)SimpleJpaRepository 实现类中的关键源码:
public class SimpleJpaRepository
implements JpaRepository, JpaSpecificationExecutor {
private final EntityManager em;
public S findOne(Example example) {
try {
return getQuery(new ExampleSpecification(example), example.getProbeType(), (Sort) null).getSingleResult();
} catch (NoResultException e) {
return null;
}
}
protected TypedQuery getQuery(Specification spec, Class domainClass,
Sort sort) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery query = builder.createQuery(domainClass);
Root root = applySpecificationToCriteria(spec, domainClass, query);
query.select(root);
if (sort != null) {
query.orderBy(toOrders(sort, root, builder));
}
return applyRepositoryMethodMetadata(em.createQuery(query));
}
......
}
(4)读 SimpleJpaRepository 源码给大家的启示
当我们学习一个 API 的时候最好顺带读一下源码,分析一下它的实现结构,这样你会有意外发现: