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。

主要涉及以下内容:

ExampleExampleMatcher 的使用方式
QueryByExampleExecutor 提供的查询接口
✅ 匹配策略的定制与字段忽略机制

虽然 QBE 功能有限,但在一些简单的动态查询场景下非常实用,代码也更简洁易懂。

完整代码已上传至 GitHub:https://github.com/eugenp/tutorials/tree/master/persistence-modules/spring-data-jpa-query


原始标题:Spring Data JPA Query by Example