1. 简介
本文将带你深入了解 Spring Data 的 Query by Example(QBE)API,这是一种基于示例对象进行查询的灵活方式。
我们会先定义测试数据结构,然后介绍 QBE 相关的核心类,最后通过几个实际例子展示它的使用方法。
2. 测试数据
我们的测试数据是一个乘客列表,包含以下字段:
First Name | Last Name | Seat Number |
---|---|---|
Jill | Smith | 50 |
Eve | Jackson | 94 |
Fred | Bloggs | 22 |
Ricki | Bobbie | 36 |
Siya | Kolisi | 85 |
3. 实体建模
我们首先创建一个 JPA 实体类 Passenger
来映射上面的数据:
@Entity
class Passenger {
@Id
@GeneratedValue
@Column(nullable = false)
private Long id;
@Basic(optional = false)
@Column(nullable = false)
private String firstName;
@Basic(optional = false)
@Column(nullable = false)
private String lastName;
@Basic(optional = false)
@Column(nullable = false)
private int seatNumber;
// constructor, getters etc.
}
当然,也可以使用其他持久化抽象来建模,但这里我们选择 JPA。
4. Query by Example API 核心接口
4.1 JpaRepository 继承关系
JpaRepository
接口扩展了 QueryByExampleExecutor
接口,从而支持 QBE 查询:
public interface JpaRepository<T, ID>
extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {}
4.2 QueryByExampleExecutor 提供的方法
这个接口新增了一系列 find
方法,都接受一个 Example
对象作为参数:
public interface QueryByExampleExecutor<T> {
<S extends T> Optional<S> findOne(Example<S> var1);
<S extends T> Iterable<S> findAll(Example<S> var1);
<S extends T> Iterable<S> findAll(Example<S> var1, Sort var2);
<S extends T> Page<S> findAll(Example<S> var1, Pageable var2);
<S extends T> long count(Example<S> var1);
<S extends T> boolean exists(Example<S> var1);
}
4.3 Example 接口
Example
是查询的核心,它由两个部分组成:
- probe:即我们要查询的实体样例对象。
- matcher:即匹配规则,控制如何匹配 probe 中的属性。
构造方法如下:
public interface Example<T> {
static <T> org.springframework.data.domain.Example<T> of(T probe) {
return new TypedExample(probe, ExampleMatcher.matching());
}
static <T> org.springframework.data.domain.Example<T> of(T probe, ExampleMatcher matcher) {
return new TypedExample(probe, matcher);
}
T getProbe();
ExampleMatcher getMatcher();
default Class<T> getProbeType() {
return ProxyUtils.getUserClass(this.getProbe().getClass());
}
}
✅ 总结:probe + matcher = query
5. 使用限制 ⚠️
虽然 QBE 很方便,但它也有一定局限性:
❌ 不支持嵌套和分组逻辑(比如 (firstName = ?0 and lastName = ?1) or seatNumber = ?2
)
❌ 字符串匹配仅支持:精确匹配、忽略大小写、前缀、后缀、包含、正则表达式
❌ 非字符串类型只支持精确匹配
这些限制决定了 QBE 更适合简单查询场景。复杂查询建议还是用 JPQL 或者 QueryDSL。
6. 示例详解
6.1. 默认精确匹配(区分大小写)
最简单的例子是直接传入一个 probe 对象:
@Test
public void givenPassengers_whenFindByExample_thenExpectedReturned() {
Example<Passenger> example = Example.of(Passenger.from("Fred", "Bloggs", null));
Optional<Passenger> actual = repository.findOne(example);
assertTrue(actual.isPresent());
assertEquals(Passenger.from("Fred", "Bloggs", 22), actual.get());
}
此时使用的是默认的 ExampleMatcher.matching()
,它会对所有非 null 属性做 严格匹配,包括大小写。
6.2. 忽略大小写匹配
如果希望忽略大小写,可以通过自定义 matcher 实现:
@Test
public void givenPassengers_whenFindByExampleCaseInsensitiveMatcher_thenExpectedReturned() {
ExampleMatcher caseInsensitiveExampleMatcher = ExampleMatcher.matchingAll().withIgnoreCase();
Example<Passenger> example = Example.of(Passenger.from("fred", "bloggs", null),
caseInsensitiveExampleMatcher);
Optional<Passenger> actual = repository.findOne(example);
assertTrue(actual.isPresent());
assertEquals(Passenger.from("Fred", "Bloggs", 22), actual.get());
}
✅ 注意:matchingAll()
和 matching()
行为一致,表示所有属性都要满足条件。
6.3. 自定义属性匹配策略
还可以针对不同字段设置不同的匹配规则,比如模糊匹配、包含等:
@Test
public void givenPassengers_whenFindByExampleCustomMatcher_thenExpectedReturned() {
Passenger jill = Passenger.from("Jill", "Smith", 50);
Passenger eve = Passenger.from("Eve", "Jackson", 95);
Passenger fred = Passenger.from("Fred", "Bloggs", 22);
Passenger siya = Passenger.from("Siya", "Kolisi", 85);
Passenger ricki = Passenger.from("Ricki", "Bobbie", 36);
ExampleMatcher customExampleMatcher = ExampleMatcher.matchingAny()
.withMatcher("firstName", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase())
.withMatcher("lastName", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase());
Example<Passenger> example = Example.of(Passenger.from("e", "s", null), customExampleMatcher);
List<Passenger> passengers = repository.findAll(example);
assertThat(passengers, contains(jill, eve, fred, siya));
assertThat(passengers, not(contains(ricki)));
}
✅ 这里用了 matchingAny()
,表示只要有一个字段匹配即可。
6.4. 忽略某些字段
有时只想根据部分字段查询,可以使用 ignorePaths
忽略其他字段:
@Test
public void givenPassengers_whenFindByIgnoringMatcher_thenExpectedReturned() {
Passenger jill = Passenger.from("Jill", "Smith", 50);
Passenger eve = Passenger.from("Eve", "Jackson", 95);
Passenger fred = Passenger.from("Fred", "Bloggs", 22);
Passenger siya = Passenger.from("Siya", "Kolisi", 85);
Passenger ricki = Passenger.from("Ricki", "Bobbie", 36);
ExampleMatcher ignoringExampleMatcher = ExampleMatcher.matchingAny()
.withMatcher("lastName", ExampleMatcher.GenericPropertyMatchers.startsWith().ignoreCase())
.withIgnorePaths("firstName", "seatNumber");
Example<Passenger> example = Example.of(Passenger.from(null, "b", null), ignoringExampleMatcher);
List<Passenger> passengers = repository.findAll(example);
assertThat(passengers, contains(fred, ricki));
assertThat(passengers, not(contains(jill));
assertThat(passengers, not(contains(eve));
assertThat(passengers, not(contains(siya));
}
✅ 这种方式非常适合动态查询,比如用户只填写了部分筛选条件。
7. 小结
在这篇文章中,我们详细讲解了 Spring Data JPA 中的 Query by Example API。
主要涉及以下内容:
✅ Example
和 ExampleMatcher
的使用方式
✅ QueryByExampleExecutor
提供的查询接口
✅ 匹配策略的定制与字段忽略机制
虽然 QBE 功能有限,但在一些简单的动态查询场景下非常实用,代码也更简洁易懂。
完整代码已上传至 GitHub:https://github.com/eugenp/tutorials/tree/master/persistence-modules/spring-data-jpa-query