Все кто использовал Spring Data сталкивались с ситуацией, когда у вас есть репозиторий для работы с сущностью и вы хотите написать универсальный find-метод для поиска по набору параметров, которые пользователь может задать или пропустить на форме поиска. Базовая реализация find методов в Spring Data находит сущности только с учетом всех параметров, не позволяя искать по ограниченному набору. Я нашел способ решить эту проблему и создал OpenSource библиотеку для быстрого использования в других проектах.

Чтобы понять проблемы, представим что мы создаем простое приложение- записную книжку, в котором определили сущность- Person с полями id, firstName, lastName, phoneNumber.

Person.java
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(of = "id")
public class Person {
    @Id
    private Long id;
    private String firstName;
    private String lastName;
    private String phoneNumber;
}


Допустим мы должны обеспечить поиск по имени, фамилии и части телефонного номера или по любой комбинации этих параметров и хотим, чтобы метод поиска не учитывал параметры, значения которых равны null. Для решения этой задачи можно пойти несколькими путями:

  1. Работать напрямую с базой данных через SQL, создать метод в сервисе, который динамически сформирует SQL запрос. Что-то вроде

    Динамический запрос
      Iterable<Person> find(String firstName, String lastName, String phoneNumber) {
        List<String> where = new ArrayList();
        List params = new ArrayList();
        if(firstName != null) {
          params.add(firstName);
          where.add("first_name = ?");
        }
        if(lastName != null) {
          params.add(lastName);
          where.add("last_name = ?");
        }
        if(phoneNumber != null) {
          params.add(phoneNumber);
          where.add("phone_number = ?");
        }
    
        String sql = "SELECT * FROM person " + (where.isEmpty() ? "" : " WHERE " + String.join(" AND ", where));
        // Вызов SQL через JDBCTemplate
        // ...
      }
                

  2. Использовать аннотацию Query для метода поиска с проверкой параметров на null.

    @Query
      @Query("SELECT p FROM Person p " +
             "WHERE " +
             "(firstName = :firstName or :firstName is null) and " +
             "(lastName = :lastName or :lastName is null) and " +
             "(phoneNumber = :phoneNumber or :phoneNumber is null)"
                )
      Iterable<Person> find(
                  @Param("firstName") String firstName,
                  @Param("lastName") String lastName,
                  @Param("phoneNumber") String phoneNumber
                );
                


  3. Создать методы поиска под все возможные комбинации параметров и вызывать нужный метод после проверки параметров на null.

    Много find методов
      @Repository
      public interface PersonRepo extends PagingAndSortingRepository<Person, Long> {
        // Методы поиска для разных комбинаций параметров
        Iterable<Person> phoneNumberContains(String number);
        Iterable<Person> lastName(String lastName);
        Iterable<Person> lastNameAndPhoneNumberContains(String lastName, String number);
        Iterable<Person> firstName(String firstName);
        Iterable<Person> firstNameAndPhoneNumberContains(String firstName, String number);
        Iterable<Person> firstNameAndLastName(String firstName, String lastName);
        Iterable<Person> firstNameAndLastNameAndPhoneNumberContains(String firstName, String lastName, String number);
    
        // Метод поиска, который определяет какой вариант сработал
        default Iterable<Person> findByFirstNameAndLastNameAndPhoneNumberContains(String firstName, String lastName, String number) {
          if(firstName == null) {
            if(lastName == null) {
              if(number == null) {
                return findAll();
              } else {
                return phoneNumberContains(number);
              }
            } else {
              if(number == null) {
                return lastName(lastName);
              } else {
                return lastNameAndPhoneNumberContains(lastName, number);
              }
            }
          } else {
            if(lastName == null) {
              if(number == null) {
                return firstName(firstName);
              } else {
                return firstNameAndPhoneNumberContains(firstName, number);
              }
            } else {
              if(number == null) {
                return firstNameAndLastName(firstName, lastName);
              } else {
                return firstNameAndLastNameAndPhoneNumberContains(firstName, lastName, number);
              }
            }
          }
        }
      }
                



Я не буду анализировать достоинства и недостатки каждого способа- они очевидны. Скажу лишь, что я выбрал третий вариант с добавлением методов поиска под каждый вариант комбинации параметров и создал OpenSource бибилиотеку, которая использует механизм Annotation Processor и на этапе компиляции и делает всю работу за вас. Чтобы воспользоваться ею- необходимо подключить библиотеку (последнюю версию смотрите на https://github.com/ukman/kolobok или https://mvnrepository.com/artifact/com.github.ukman/kolobok).

<dependency>
    <groupId>com.github.ukman</groupId>
    <artifactId>kolobok</artifactId>
    <version>0.1.2</version>
    <scope>compile</scope>
</dependency>

Затем нужно пометить метод, который должен работать по-новому аннотацией @FindWithOptionalParams.

@Repository
public interface PersonRepo extends PagingAndSortingRepository<Person, Long> {
  @FindWithOptionalParams
  Iterable<Person> findByFirstNameAndLastNameAndPhoneNumberContains(String firstName, String lastName, String number);
}

Библиотека сама сгенерирует все методы поиска и default реализацию с проверкой параметров на null и вызовом необходимого метода.

P.S.: напишите в комментариях, какие бы еще аннотации могли бы упростить вам работу со Spring-ом, возможно их тоже добавлю.