Перейти к основному содержимому

Spring Данные JPA @Query

· 11 мин. чтения

1. Обзор

Spring Data предоставляет множество способов определить запрос, который мы можем выполнить. Одним из них является аннотация @Query .

В этом руководстве мы покажем, как использовать аннотацию @Query в Spring Data JPA для выполнения как JPQL, так и собственных запросов SQL.

Мы также покажем, как построить динамический запрос, когда аннотации @Query недостаточно.

2. Выберите запрос

Чтобы определить SQL для выполнения для метода репозитория Spring Data, мы можем аннотировать метод аннотацией @Query — его атрибут value содержит JPQL или SQL для выполнения.

Аннотация @Query имеет приоритет над именованными запросами, которые снабжены аннотацией @NamedQuery или определены в файле orm.xml .

Хороший подход — разместить определение запроса непосредственно над методом внутри репозитория, а не внутри нашей модели предметной области в виде именованных запросов. Репозиторий отвечает за сохраняемость, поэтому лучше хранить эти определения.

2.1. JPQL

По умолчанию определение запроса использует JPQL.

Давайте рассмотрим простой метод репозитория, который возвращает активные сущности пользователя из базы данных:

@Query("SELECT u FROM User u WHERE u.status = 1")
Collection<User> findAllActiveUsers();

2.2. Родной

Мы также можем использовать собственный SQL для определения нашего запроса. Все, что нам нужно сделать, это установить значение атрибута nativeQuery в true и определить собственный SQL-запрос в атрибуте value аннотации:

@Query(
value = "SELECT * FROM USERS u WHERE u.status = 1",
nativeQuery = true)
Collection<User> findAllActiveUsersNative();

3. Определите порядок в запросе

Мы можем передать дополнительный параметр типа Sort в объявление метода Spring Data с аннотацией @Query . Он будет переведен в предложение ORDER BY , которое будет передано в базу данных.

3.1. Сортировка предоставленных и производных методов JPA

Для методов, которые мы получаем из коробки, таких как findAll(Sort) или тех, которые генерируются путем разбора сигнатур методов, мы можем использовать только свойства объекта для определения нашей сортировки :

userRepository.findAll(Sort.by(Sort.Direction.ASC, "name"));

Теперь представьте, что мы хотим отсортировать по длине свойства имени:

userRepository.findAll(Sort.by("LENGTH(name)"));

Когда мы выполним приведенный выше код, мы получим исключение:

org.springframework.data.mapping.PropertyReferenceException: свойство LENGTH (имя) не найдено для типа User!

3.2. JPQL

Когда мы используем JPQL для определения запроса, Spring Data может без проблем обрабатывать сортировку — все, что нам нужно сделать, это добавить параметр метода типа Sort :

@Query(value = "SELECT u FROM User u")
List<User> findAllUsers(Sort sort);

Мы можем вызвать этот метод и передать параметр Sort , который упорядочит результат по свойству name объекта User :

userRepository.findAllUsers(Sort.by("name"));

И поскольку мы использовали аннотацию @Query , мы можем использовать тот же метод для получения отсортированного списка пользователей по длине их имен:

userRepository.findAllUsers(JpaSort.unsafe("LENGTH(name)"));

Крайне важно, чтобы мы использовали JpaSort.unsafe() для создания экземпляра объекта Sort .

Когда мы используем:

Sort.by("LENGTH(name)");

тогда мы получим точно такое же исключение, как мы видели выше для метода findAll() .

Когда Spring Data обнаруживает небезопасный порядок сортировки для метода, использующего аннотацию @Query , он просто добавляет предложение сортировки к запросу — он пропускает проверку того, принадлежит ли свойство для сортировки модели предметной области.

3.3. Родной

Когда аннотация @Query использует собственный SQL, определение Sort невозможно .

Если мы это сделаем, мы получим исключение:

org.springframework.data.jpa.repository.query.InvalidJpaQueryMethodException: нельзя использовать собственные запросы с динамической сортировкой и/или разбиением на страницы

Как говорится в исключении, сортировка не поддерживается для нативных запросов. Сообщение об ошибке дает нам подсказку, что разбиение на страницы также вызовет исключение.

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

4. Пагинация

Разбиение на страницы позволяет нам возвращать только подмножество всего результата на странице . Это полезно, например, при навигации по нескольким страницам данных на веб-странице.

Еще одно преимущество разбивки на страницы заключается в том, что объем данных, отправляемых с сервера клиенту, сводится к минимуму. Отправляя меньшие фрагменты данных, мы обычно видим улучшение производительности.

4.1. JPQL

Использовать разбиение на страницы в определении запроса JPQL очень просто:

@Query(value = "SELECT u FROM User u ORDER BY id")
Page<User> findAllUsersWithPagination(Pageable pageable);

Мы можем передать параметр PageRequest , чтобы получить страницу данных.

Разбиение на страницы также поддерживается для собственных запросов, но требует небольшой дополнительной работы.

4.2. Родной

Мы можем включить разбиение на страницы для нативных запросов, объявив дополнительный атрибут countQuery .

Это определяет SQL для выполнения для подсчета количества строк во всем результате:

@Query(
value = "SELECT * FROM Users ORDER BY id",
countQuery = "SELECT count(*) FROM Users",
nativeQuery = true)
Page<User> findAllUsersWithPagination(Pageable pageable);

4.3. Версии Spring Data JPA до 2.0.4

Приведенное выше решение для собственных запросов отлично работает для Spring Data JPA версии 2.0.4 и более поздних версий.

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

Мы можем преодолеть это, добавив дополнительный параметр для разбиения на страницы внутри нашего запроса:

@Query(
value = "SELECT * FROM Users ORDER BY id \n-- #pageable\n",
countQuery = "SELECT count(*) FROM Users",
nativeQuery = true)
Page<User> findAllUsersWithPagination(Pageable pageable);

В приведенном выше примере мы добавляем «\n– #pageable\n» в качестве заполнителя для параметра нумерации страниц. Это сообщает Spring Data JPA, как анализировать запрос и вводить параметр pageable. Это решение работает для базы данных H2 .

Мы рассмотрели, как создавать простые запросы на выборку с помощью JPQL и собственного SQL. Далее мы покажем, как определить дополнительные параметры.

5. Индексированные параметры запроса

Есть два возможных способа передачи параметров метода в наш запрос: индексированные и именованные параметры.

В этом разделе мы рассмотрим индексированные параметры.

5.1. JPQL

Для индексированных параметров в JPQL Spring Data будет передавать параметры метода в запрос в том же порядке, в котором они появляются в объявлении метода :

@Query("SELECT u FROM User u WHERE u.status = ?1")
User findUserByStatus(Integer status);

@Query("SELECT u FROM User u WHERE u.status = ?1 and u.name = ?2")
User findUserByStatusAndName(Integer status, String name);

Для вышеуказанных запросов параметр метода состояния будет присвоен параметру запроса с индексом 1, а параметр метода имени будет присвоен параметру запроса с индексом 2 .

5.2. Родной

Индексированные параметры для нативных запросов работают точно так же, как и для JPQL:

@Query(
value = "SELECT * FROM Users u WHERE u.status = ?1",
nativeQuery = true)
User findUserByStatusNative(Integer status);

В следующем разделе мы покажем другой подход: передачу параметров по имени.

6. Именованные параметры

Мы также можем передавать параметры метода в запрос, используя именованные параметры. Мы определяем их с помощью аннотации @Param внутри объявления метода нашего репозитория.

Каждый параметр с аннотацией @Param должен иметь строку значения, совпадающую с соответствующим именем параметра запроса JPQL или SQL. Запрос с именованными параметрами легче читается и менее подвержен ошибкам, если запрос нуждается в рефакторинге.

6.1. JPQL

Как упоминалось выше, мы используем аннотацию @Param в объявлении метода для сопоставления параметров, определенных по имени в JPQL, с параметрами из объявления метода:

@Query("SELECT u FROM User u WHERE u.status = :status and u.name = :name")
User findUserByStatusAndNameNamedParams(
@Param("status") Integer status,
@Param("name") String name);

Обратите внимание, что в приведенном выше примере мы определили параметры нашего SQL-запроса и метода с одинаковыми именами, но это не требуется, если строки значений одинаковы:

@Query("SELECT u FROM User u WHERE u.status = :status and u.name = :name")
User findUserByUserStatusAndUserName(@Param("status") Integer userStatus,
@Param("name") String userName);

6.2. Родной

Для нативного определения запроса нет разницы в том, как мы передаем параметр через имя в запрос по сравнению с JPQL — мы используем аннотацию @Param :

@Query(value = "SELECT * FROM Users u WHERE u.status = :status and u.name = :name", 
nativeQuery = true)
User findUserByStatusAndNameNamedParamsNative(
@Param("status") Integer status, @Param("name") String name);

7. Параметр сбора

Давайте рассмотрим случай, когда предложение where нашего запроса JPQL или SQL содержит ключевое слово IN (или NOT IN ):

SELECT u FROM User u WHERE u.name IN :names

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

@Query(value = "SELECT u FROM User u WHERE u.name IN :names")
List<User> findUserByNameList(@Param("names") Collection<String> names);

Поскольку параметр является Collection , его можно использовать с List, HashSet и т. д.

Далее мы покажем, как изменять данные с помощью аннотации @ Modifying .

8. Обновление запросов с помощью @Modifying

Мы можем использовать аннотацию @ Query для изменения состояния базы данных, также добавив аннотацию @ Modifying к методу репозитория.

8.1. JPQL

Метод репозитория, изменяющий данные, имеет два отличия от запроса на выборку — он имеет аннотацию @Modifying и, конечно же, запрос JPQL использует update вместо select :

@Modifying
@Query("update User u set u.status = :status where u.name = :name")
int updateUserSetStatusForName(@Param("status") Integer status,
@Param("name") String name);

Возвращаемое значение определяет, сколько строк обновлено при выполнении запроса. Внутри запросов на обновление можно использовать как индексированные, так и именованные параметры.

8.2. Родной

Мы можем изменить состояние базы данных также с помощью собственного запроса. Нам просто нужно добавить аннотацию @Modifying :

@Modifying
@Query(value = "update Users u set u.status = ? where u.name = ?",
nativeQuery = true)
int updateUserSetStatusForNameNative(Integer status, String name);

8.3. Вставки

Чтобы выполнить операцию вставки, мы должны применить @Modifying и использовать собственный запрос, поскольку INSERT не является частью интерфейса JPA :

@Modifying
@Query(
value =
"insert into Users (name, age, email, status) values (:name, :age, :email, :status)",
nativeQuery = true)
void insertUser(@Param("name") String name, @Param("age") Integer age,
@Param("status") Integer status, @Param("email") String email);

9. Динамический запрос

Часто мы сталкиваемся с необходимостью построения операторов SQL на основе условий или наборов данных, значения которых известны только во время выполнения. И в этих случаях мы не можем просто использовать статический запрос.

9.1. Пример динамического запроса

Например, давайте представим ситуацию, когда нам нужно выбрать всех пользователей, чья электронная почта LIKE , из набора, определенного во время выполнения — email1 , email2 , …, emailn :

SELECT u FROM User u WHERE u.email LIKE '%email1%' 
or u.email LIKE '%email2%'
...
or u.email LIKE '%emailn%'

Поскольку набор создается динамически, мы не можем знать во время компиляции, сколько предложений LIKE нужно добавить.

В этом случае мы не можем просто использовать аннотацию @Query , поскольку мы не можем предоставить статическую инструкцию SQL.

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

9.2. Пользовательские репозитории и API критериев JPA

К счастью для нас, Spring предоставляет способ расширения базового репозитория за счет использования пользовательских интерфейсов фрагментов. Затем мы можем связать их вместе, чтобы создать составной репозиторий .

Мы начнем с создания пользовательского интерфейса фрагмента:

public interface UserRepositoryCustom {
List<User> findUserByEmails(Set<String> emails);
}

И тогда мы реализуем это:

public class UserRepositoryCustomImpl implements UserRepositoryCustom {

@PersistenceContext
private EntityManager entityManager;

@Override
public List<User> findUserByEmails(Set<String> emails) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> user = query.from(User.class);

Path<String> emailPath = user.get("email");

List<Predicate> predicates = new ArrayList<>();
for (String email : emails) {
predicates.add(cb.like(emailPath, email));
}
query.select(user)
.where(cb.or(predicates.toArray(new Predicate[predicates.size()])));

return entityManager.createQuery(query)
.getResultList();
}
}

Как показано выше, мы использовали JPA Criteria API для построения нашего динамического запроса.

Кроме того, нам нужно обязательно включить постфикс Impl в имя класса. Spring будет искать реализацию UserRepositoryCustom как UserRepositoryCustomImpl . Поскольку фрагменты сами по себе не являются репозиториями, Spring полагается на этот механизм для поиска реализации фрагмента.

9.3. Расширение существующего репозитория

Обратите внимание, что все методы запроса из раздела 2 по раздел 7 находятся в UserRepository .

Итак, теперь мы интегрируем наш фрагмент, расширив новый интерфейс в UserRepository :

public interface UserRepository extends JpaRepository<User, Integer>, UserRepositoryCustom {
// query methods from section 2 - section 7
}

9.4. Использование репозитория

И, наконец, мы можем вызвать наш метод динамического запроса:

Set<String> emails = new HashSet<>();
// filling the set with any number of items

userRepository.findUserByEmails(emails);

Мы успешно создали составной репозиторий и вызвали наш пользовательский метод.

10. Заключение

В этой статье мы рассмотрели несколько способов определения запросов в методах репозитория Spring Data JPA с использованием аннотации @Query .

Мы также узнали, как реализовать собственный репозиторий и создать динамический запрос.

Как всегда, полные примеры кода, используемые в этой статье, доступны на GitHub .