«1. Введение

В этом руководстве мы узнаем, как запрашивать данные с помощью Spring Data Query by Example API.

Сначала мы определим схему данных, которые мы хотим запросить. Далее мы рассмотрим несколько соответствующих классов из Spring Data. А затем мы рассмотрим несколько примеров.

Начнем!

2. Тестовые данные

Наши тестовые данные представляют собой список имен пассажиров, а также места, которые они занимали.

First Name Last Name Seat Number
Jill Smith 50
Eve Jackson 94
Fred Bloggs 22
Ricki Bobbie 36
Siya Kolisi 85

3. Домен

Давайте создадим нужный репозиторий данных Spring и предоставим класс нашего домена и тип идентификатора.

Для начала мы смоделировали нашего Passenger как объект JPA:

@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

Во-первых, давайте взглянем на интерфейс JpaRepository. Как мы видим, он расширяет интерфейс QueryByExampleExecutor для поддержки запроса на примере:

public interface JpaRepository<T, ID>
  extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {}

Этот интерфейс представляет больше вариантов метода find(), с которым мы знакомы по Spring Data. Однако каждый метод также принимает экземпляр примера:

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);
}

Во-вторых, интерфейс примера предоставляет методы для доступа к зонду и ExampleMatcher.

Важно понимать, что зонд является экземпляром нашей сущности:

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());
    }
}

Итак, наш зонд и наш ExampleMatcher вместе определяют наш запрос.

5. Ограничения

Как и все, API Query by Example имеет некоторые ограничения. Например:

    Операторы вложенности и группировки не поддерживаются, например: (firstName = ?0 и lastName = ?1) или seatNumber = ?2 Сопоставление строк включает только точные, нечувствительные к регистру, начала, окончания, содержит и regex Все типы, кроме String, являются только точными совпадениями

Теперь, когда мы немного познакомились с API и его ограничениями, давайте углубимся в некоторые примеры.

6. Примеры

6.1. Сопоставление с учетом регистра

Давайте начнем с простого примера и поговорим о поведении по умолчанию:

@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());
}

В частности, статический метод Example.of() создает пример с использованием ExampleMatcher.matching().

Другими словами, будет выполнено точное совпадение со всеми ненулевыми свойствами Passenger. Таким образом, сопоставление чувствительно к регистру в свойствах String.

Однако было бы не слишком полезно, если бы все, что мы могли сделать, это точное совпадение всех ненулевых свойств.

Здесь на помощь приходит ExampleMatcher. Создав собственный ExampleMatcher, мы можем настроить его поведение в соответствии с нашими потребностями.

6.2. Сопоставление без учета регистра

Имея это в виду, давайте посмотрим на другой пример, на этот раз с использованием withIgnoreCase() для достижения соответствия без учета регистра:

@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());
}

Обратите внимание, что в этом примере мы сначала вызвали ExampleMatcher. MatchingAll() — ведет себя так же, как ExampleMatcher.matching(), который мы использовали в предыдущем примере.

6.3. Пользовательское сопоставление

Мы также можем настроить поведение нашего сопоставления для каждого свойства и сопоставить любое свойство с помощью ExampleMatcher.matchingAny():

@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)));
}

6.4. Игнорирование свойств

С другой стороны, мы также можем запрашивать только подмножество наших свойств.

Мы достигаем этого, игнорируя некоторые свойства, используя ExampleMatcher.ignorePaths(String…paths):

@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. Заключение

В этой статье мы продемонстрировали, как использовать Query by Example API.

Мы продемонстрировали, как использовать Example и ExampleMatcher вместе с интерфейсом QueryByExampleExecutor для запроса таблицы с использованием примера экземпляра данных.

В заключение, вы можете найти код на GitHub.