JPA 之 QueryByExampleExecutor
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,分别做如下解释:
- 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 视图,看看⾥⾯对外暴露的⽅法都有哪些。
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 时,是否参与过滤;
- 当该选项值是 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 规则。
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 时需要考虑的因素
- Null 值的处理:当某个条件值为 Null 时,是应当忽略这个过滤条件,还是应当去匹配数据库表中该字段值是 Null 的记录呢?
- 忽略某些属性值:⼀个实体对象,有许多个属性,是否每个属性都参与过滤?是否可以忽略某些属性?
- 不同的过滤⽅式:同样是作为 String 值,可能“姓名”希望精确匹配,“地址”希望模糊匹配,如何做到?
ExampleMatcher补充
nullHandler:null值处理方式,枚举类型
- INCLUDE(包括)
-
IGNORE(忽略)
标识作为条件的实体对象中,一个属性值为null 时,标识是否参与过滤
defaultStringMatcher: 默认字符串匹配方式,枚举类型,有6个可选值:
- DEFAULT(默认,效果同EXACT) :
- EXACT(相等): uname=?1
- STARTING(开始匹配): LOWER(uname) = LOWER(?1)
- ENDING(结束匹配): LOWER(uname) like = LOWER(?1)+'%'
- CONTAINING(包含,模糊匹配): uname like '%' + ?1
- REGEX(正则表达式) : uname like '%' + ?1 + '%'
对所有字符串属性有效,除非该属性在propertySpecifiers中单独定义匹配方式:
- defaultIgnoreCase: 默认大小写忽略方式,boolean,false不忽略大小写,propertySpecifiers中可单独定义
- propertySpecifiers: 各属性自定义查询方式包含:属性名、字符串匹配方式、大小写忽略方式、属性转换器
- ignorePaths: 忽略属性列表,忽略的属性不参与查询
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方法,里面参数的含义分别是:
- root:得到查询的根,root.get(“变量名”),根据变量名查询。
- query:构建查询的顶层规则(where,from等)
-
builder:构建查询的底层规则(equal,like,in等)
后面的getRestriction()方法的作用就是返回一个predicate对象。
结果:
image.png
二、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);
}
结果:
以上条件不变,分页展示查询出来的数据。
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。
依旧使用上面的模糊查询,我们把查询结果按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);
}
结果:
最后一个方法是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);
}
结果:
以上都是使用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);
}
结果:
使用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
至于使用哪种就看实际场景和个人习惯了。