Необязательные параметры в репозиториях Spring Data

    Все кто использовал 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-ом, возможно их тоже добавлю.
    Поделиться публикацией

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 12

      +3
      А почему не рассматриваете Specifications (или Querydsl)? Для фильтрации по динамическим параметрам вполне себе вариант. По крайней мере «магии» там поменьше, чем в вашем варианте с аннотацией.
        –2

        Честно говоря ни разу не использовал Specification- как правило если мне не хватает в Spring Data обычных find методов, то мне проще всегда сразу взяться за SQL. Именно поэтому, чтобы реже расчехлять SQL-пушку, хотелось побольше "магии" для find методов.

          0

          А вы посмотрите. Specification или QueryDSL – как раз то, что Вам нужно.

        –2
        то есть, как это, уже конец статьи?!

        «findByFirstNameAndLastNameAndPhoneNumberContains» уже сто лет в обед и, насколько я помню, раньше даже "@FindWithOptionalParams" не требовалась…

        P.S.: понял, аннотация для колобка…
          0

          Если в стандартной реализации вызовите


          persons = findByFirstNameAndLastNameAndPhoneNumberContains("Иван", null, "") 

          То вы не получите всех людей с именем "Иван". В некоторых реализациях вы получите Иванов у которых в поле lastName стоит null.

          0

          Есть же ещё поиск по Example, по сути AND по всем not null полям.

            0

            Да, все верно, но по сути цель статьи- не анализ всех существующих методов, а демонстрация того, что показалось наиболее удобным для меня, если нравится- пользуйтесь, если не нравится- используйте свое.

            0
            Я всё же за явное создание n-го количества отдельных методов с явной расстановкой Nullable\Nonnull аннотаций. В этом случае можно будет добавить автоматическую генерацию индексов для покрытия запросов.
            В вашем же случае возможны артефакты, когда был создан индекс на столбцы A+B+C, затем значение для A не передали, в запросе пошёл поиск по B+C, индекс не применился и мы получили «тыкву».
              0

              Мой опыт говорит что автоматическая расстановка индексов приводит к большому количеству индексов. У каждого свой дзен и я обычно на этапе разработки индексы расставляю только на foreign key, остальное расставляю опираясь на реальную статистику запросов.
              Premature optimization is the root of all evil

              0
              Имхо, Criteria API и Specification под эту задачу самое то, плюс они уже есть в Spring Data — не нужно тащить сторонние библиотеки. А если будет 7-10 параметров поиска, то название метода уже в 120 симоволов может не влезть :)
                0
                Я правильно понимаю, что нет возможности указать какие именно из параметров опциональные а какие нет?
                  0

                  Все опциональные: если паматер null- значит он не участвует в поиске.

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

                Самое читаемое