|
此版本仍在开发中,尚未被视为稳定版本。如需使用最新的稳定版本,请使用 Spring Data Relational 4.0.4! |
按示例查询
介绍
本章介绍了按示例查询(Query by Example)并说明了如何使用它。
按示例查询(Query by Example,QBE)是一种用户友好的查询技术,具有简单的接口。 它支持动态创建查询,且无需编写包含字段名称的查询语句。 事实上,按示例查询完全不需要你使用特定数据存储的查询语言来编写查询。
| 本章介绍了按示例查询(Query by Example)的核心概念。 这些信息来自 Spring Data Commons 模块。 根据您所使用的数据库,字符串匹配支持可能会受到限制。 |
用法
按示例查询(Query by Example)API 由四个部分组成:
-
Probe:一个已填充字段的领域对象的实际示例。
-
ExampleMatcher:ExampleMatcher包含如何匹配特定字段的详细信息。 它可以在多个 Example 之间重复使用。 -
Example:一个Example由探针(probe)和ExampleMatcher组成。 它用于创建查询。 -
FetchableFluentQuery:一个FetchableFluentQuery提供了流式 API,允许对从Example派生的查询进行进一步自定义。 使用该流式 API,您可以为查询指定排序、投影和结果处理方式。
按示例查询(Query by Example)非常适合以下几种使用场景:
-
使用一组静态或动态约束条件查询您的数据存储。
-
频繁重构领域对象,而无需担心破坏现有查询。
-
独立于底层数据存储 API 工作。
按示例查询(Query by Example)也存在若干限制:
-
不支持嵌套或分组的属性约束,例如
firstname = ?0 or (firstname = ?1 and lastname = ?2)。 -
不支持匹配集合或映射。
-
针对特定存储的字符串匹配支持。 根据您使用的数据库,字符串匹配可以支持 starts(开头匹配)/contains(包含)/ends(结尾匹配)/regex(正则表达式)等操作。
-
其他属性类型的精确匹配。
在开始使用按示例查询(Query by Example)之前,您需要有一个领域对象。 首先,为您的仓库创建一个接口,如下例所示:
public class Person {
@Id
private String id;
private String firstname;
private String lastname;
private Address address;
// … getters and setters omitted
}
前面的示例展示了一个简单的领域对象。
您可以使用它来创建一个Example。
默认情况下,值为null的字段会被忽略,字符串则使用存储特定的默认方式匹配。
将属性包含到示例查询(Query by Example)条件中是基于可空性的。
使用原始类型的属性(int、double、…)始终会被包含,除非 ExampleMatcher 忽略了该属性路径。 |
示例可以通过使用 of 工厂方法或通过使用 ExampleMatcher 来构建。Example 是不可变的。
以下列表展示了一个简单的示例:
Person person = new Person(); (1)
person.setFirstname("Dave"); (2)
Example<Person> example = Example.of(person); (3)
| 1 | 创建域对象的新实例。 |
| 2 | 设置要查询的属性。 |
| 3 | 创建 Example。 |
你可以通过使用仓库来运行示例查询。
为此,让你的仓库接口继承 QueryByExampleExecutor<T>。
以下代码片段展示了 QueryByExampleExecutor 接口的部分内容:
QueryByExampleExecutorpublic interface QueryByExampleExecutor<T> {
<S extends T> S findOne(Example<S> example);
<S extends T> Iterable<S> findAll(Example<S> example);
// … more functionality omitted.
}
示例匹配器
示例不仅限于默认设置。
你可以通过使用 ExampleMatcher 来指定自己的字符串匹配、空值处理以及针对特定属性的设置,如下例所示:
Person person = new Person(); (1)
person.setFirstname("Dave"); (2)
ExampleMatcher matcher = ExampleMatcher.matching() (3)
.withIgnorePaths("lastname") (4)
.withIncludeNullValues() (5)
.withStringMatcher(StringMatcher.ENDING); (6)
Example<Person> example = Example.of(person, matcher); (7)
| 1 | 创建域对象的新实例。 |
| 2 | 设置属性。 |
| 3 | 创建一个 ExampleMatcher,以期望所有值都匹配。
即使在此阶段不进行额外配置,它也是可用的。 |
| 4 | 构造一个新的 ExampleMatcher 以忽略 lastname 属性路径。 |
| 5 | 构造一个新的 ExampleMatcher,以忽略 lastname 属性路径并包含空值。 |
| 6 | 构造一个新的 ExampleMatcher,以忽略 lastname 属性路径,包含空值,并执行后缀字符串匹配。 |
| 7 | 基于领域对象和已配置的 Example 创建一个新的 ExampleMatcher。 |
默认情况下,ExampleMatcher 要求探针(probe)上设置的所有值都必须匹配。
如果您希望获得与任意一个隐式定义的谓词相匹配的结果,请使用 ExampleMatcher.matchingAny()。
您可以为单个属性(例如“firstname”和“lastname”,或者嵌套属性如“address.city”)指定行为。 您可以通过匹配选项和大小写敏感性对其进行调整,如下例所示:
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("firstname", endsWith())
.withMatcher("lastname", startsWith().ignoreCase());
}
配置匹配器选项的另一种方式是使用 Lambda 表达式(Java 8 引入)。 这种方法会创建一个回调,要求实现者修改匹配器。 你无需返回匹配器,因为配置选项已保存在匹配器实例内部。 以下示例展示了一个使用 Lambda 表达式的匹配器:
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("firstname", match -> match.endsWith())
.withMatcher("firstname", match -> match.startsWith());
}
通过Example创建的查询使用配置的合并视图。
默认匹配设置可以在ExampleMatcher级别进行配置,而特定设置则可应用于具体的属性路径。
在ExampleMatcher上设置的配置会被属性路径设置所继承,除非这些属性路径明确地定义了自己的设置。
属性路径上的设置优先级高于默认设置。
下表描述了各种ExampleMatcher设置的作用范围:
| 设置 | 作用域 |
|---|---|
Null-handling |
|
字符串匹配 |
|
忽略属性 |
属性路径 |
大小写敏感性 |
|
值转换 |
属性路径 |
流畅 API
QueryByExampleExecutor 通过 Example 定义了流畅的查询方法,用于基于 findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction) 实例灵活地执行查询。
与其他方法一样,它会执行一个从 Example 派生的查询。
然而,query 方法允许你控制那些通常无法动态控制的查询执行细节。
你可以通过调用 FetchableFluentQuery 的各种中间方法和终止方法来实现这一点。
中间方法
-
sortBy:为您的结果应用排序。 重复调用该方法会依次追加每个Sort(注意,使用已排序的page(Pageable)调用Pageable会覆盖之前的所有排序顺序)。 -
limit:限制结果数量。 -
as:指定要读取或投影到的类型。 -
project:限制查询的属性。
终结方法
-
first、firstValue:返回第一个值。first返回一个Optional<T>,如果查询未产生任何结果,则返回Optional.empty()。firstValue是其可空变体,无需使用Optional。 -
one、oneValue:返回单个值。one返回一个Optional<T>,如果查询未产生任何结果,则返回Optional.empty()。oneValue是其可为空的变体,无需使用Optional。 如果找到多个匹配项,则抛出IncorrectResultSizeDataAccessException。 -
all:将所有结果以List<T>的形式返回。 -
page(Pageable):将所有结果以Page<T>的形式返回。 -
slice(Pageable):将所有结果以Slice<T>的形式返回。 -
scroll(ScrollPosition):使用滚动(偏移量、键集)以Window<T>的形式检索结果。 -
stream():返回一个Stream<T>以惰性方式处理结果。 该流是有状态的,使用后必须关闭。 -
count和exists:返回匹配实体的数量,或判断是否存在任何匹配项。
| 中间方法和终端方法必须在查询函数内调用。 |
Page 排序的投影 lastnamePage<CustomerProjection> page = repository.findBy(example,
q -> q.as(CustomerProjection.class)
.page(PageRequest.of(0, 20, Sort.by("lastname")))
);
lastname 排序后可能存在的多个结果中的最后一个Optional<Customer> match = repository.findBy(example,
q -> q.sortBy(Sort.by("lastname").descending())
.first()
);
这是一个示例:
Employee employee = new Employee(); (1)
employee.name= "Frodo";
Example<Employee> example = Example.of(employee); (2)
repository.findAll(example); (3)
// do whatever with the result
| 1 | 使用条件创建一个领域对象(null 字段将被忽略)。 |
| 2 | 使用领域对象创建一个Example。 |
| 3 | 通过仓库执行查询(使用 findOne 查询单个项)。 |
这说明了如何使用一个领域对象来构建一个简单的探针(probe)。
在此例中,它将根据 Employee 对象的 name 字段等于 Frodo 进行查询。
null 字段将被忽略。
Employee employee = new Employee();
employee.name = "Baggins";
employee.role = "ring bearer";
ExampleMatcher matcher = matching() (1)
.withMatcher("name", endsWith()) (2)
.withIncludeNullValues() (3)
.withIgnorePaths("role"); (4)
Example<Employee> example = Example.of(employee, matcher); (5)
repository.findAll(example);
// do whatever with the result
| 1 | 创建一个自定义的ExampleMatcher,使其匹配所有字段(使用matchingAny()来匹配任意字段) |
| 2 | 对于 name 字段,请使用一个通配符,该通配符将匹配字段的结尾部分 |
| 3 | 将列与 null 进行匹配(别忘了在关系型数据库中,NULL 并不等于 NULL)。 |
| 4 | 在构建查询时忽略 role 字段。 |
| 5 | 将自定义的 ExampleMatcher 插入到探针中。 |
也可以对任意属性应用 withTransform(),从而在构建查询之前对该属性进行转换。
例如,在创建查询之前,您可以对基于 toUpperCase() 的属性应用 String 方法。
当您事先不知道查询所需的所有字段时,Query By Example(按示例查询)真正展现出其优势。 如果您正在构建一个网页过滤器,允许用户选择字段,那么 Query By Example 是一种将这种灵活性高效地转化为查询的绝佳方式。