Sharing knowledge

Ищем с помощью Spring Data JPA

Рассмотрим подробнее одну из наиболее полезных вещей в Spring Data JPA - генерация JPQL-запросов на основе имени метода.

Spring Data JPA умеет автоматически генерировать запросы используя для подсказки название метода.

Например, метод User.findByLoginAndPassword сгенерирует примерно следующий код:

FROM User u where u.login = :login and password = :password

Вообще Spring Data JPA пытается быть умным, поэтому реализация findBy{…} методов ищется следующим образом:

  1. Сначала смотрится аннотация @Query на объявлении метода, если она есть, то используется.
  2. Затем смотрится аннотация @NamedQuery с именем вида Entity.findMethodName, для вышеприведённого случая это будет User.findByLoginAndPassword.
  3. Если ничего не нашли. то по сигнатуре метода генерируется запрос.

У @Query следующие плюсы:

  1. Позволяет не засорять объявление доменной сущности.
  2. Сильно помогает, если у нас в запросе есть неявные джойны, потому что для таких запросов Spring Data JPA не умеет корректно генерировать запрос SELECT COUNT(*), который нужен в тех случаях, когда метод должен вернуть Page.

Очевидно, что при использовании запросов нам необходимо каким-то образом указывать параметры для запросов. Для этого есть аннотация @Param:

@Query("select u from User u where u.login = :login and u.password = :password")
Page<User> findByLoginAndPassword(@Param("login") String login, @Param("password") String password);

Кроме того, Spring Data JPA поддерживает отличную концепцию спецификаций.

Спецификации позволяют делать составлять сложные запросы из набора простых.

Для поддержки спецификаций необходимо объявить метод в репозитории:

Page<User> findAll(Specification<User> spec, Pageable pageable);

Спецификация по сути является фильтром и позволяет комбинирование фильтров, что даёт мощный инструмент для построения запросов.

Пример использования спецификаций:

Объявляем наши спецификации:

public static Specification<User> firstNameOrLastNameOrLoginLike(final String search) {
    return new Specification<User>() {
        @Override
        public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder builder) {

            Predicate loginPredicate = builder.like(root.get(User_.login), search);
            Predicate firstnamePredicate = builder.like(root.get(User_.firstname), search);
            Predicate lastnamePredicate = builder.like(root.get(User_.lastname), search);

            return builder.or(loginPredicate, firstnamePredicate, lastnamePredicate);
        }
    };
}

public static Specification<User> hasRole(final Role role) {
    return new Specification<User>() {
        @Override
        public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
            return builder.equal(root.get(User_.role), role);
        }
    };
}

И в нашем сервисе комбинируем их:

public Page<User> searchUser(Role role, String search, Pageable pageable) {
    Specifications<User> mainSpec = where(hasRole(role));

    // уточняем запрос, если была передана строка для поиска
    if (StringUtils.isNotBlank(search)) {
        mainSpec = mainSpec.and(firstNameOrLastNameOrLoginLike(search));
    } 

    return userRepository.findAll(mainSpec, pageable);
}