@Query, JPQL и нативные запросы
Производных запросов хватает не всегда. Для сложных условий есть @Query: вы пишете JPQL (объектный SQL) или нативный SQL прямо над методом.
Суть: @Query задаёт запрос явно. JPQL оперирует сущностями и полями (портируемо между БД), нативный SQL — реальными таблицами (когда нужны возможности конкретной СУБД).
Имя метода findByDepartmentNameAndSalaryGreaterThanOrderBySalaryDesc читать невозможно. Когда логика запроса сложная — соединения, агрегации, подзапросы — на помощь приходит аннотация @Query, где вы пишете запрос текстом.
JPQL — запрос над объектами
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.age > :minAge ORDER BY u.name")
List<User> findOlderThan(@Param("minAge") int minAge);
@Query("SELECT u FROM User u WHERE u.email = :email")
Optional<User> findByEmailExplicit(@Param("email") String email);
}
Обратите внимание: в JPQL пишется User (имя класса-сущности), а не users (имя таблицы), и u.age (поле объекта), а не колонка. JPQL работает в терминах объектной модели — поэтому он переносим между базами.
Нативный SQL
Когда нужны возможности конкретной СУБД (оконные функции, специфический синтаксис), используют нативный запрос:
@Query(value = "SELECT * FROM users WHERE age > :minAge", nativeQuery = true)
List<User> findOlderNative(@Param("minAge") int minAge);
Здесь users — реальная таблица, nativeQuery = true говорит выполнять текст как есть. Плата за мощь — потеря переносимости между СУБД.
Как работает под капотом
JPQL-запрос Hibernate сначала разбирает и транслирует в SQL под вашу СУБД, подставляя имена таблиц и колонок из карты сущностей. Нативный запрос идёт в базу почти без изменений. Именованные параметры (:minAge) безопасно подставляются через подготовленный запрос — это защищает от SQL-инъекций.
@Query JPQL: "SELECT u FROM User u WHERE u.age > :minAge"
| Hibernate транслирует
v
SQL: SELECT * FROM users WHERE age > ?
| :minAge -> ? (prepared statement, без инъекций)
v
СУБД выполняет, возвращает строки -> объекты User
Смоделируем безопасную подстановку параметров и фильтр:
# JPQL-подобный запрос с именованным параметром
users = [
{"name": "Анна", "age": 30},
{"name": "Борис", "age": 25},
{"name": "Вера", "age": 45},
]
def query_older_than(params):
min_age = params["minAge"] # именованный параметр :minAge
result = [u for u in users if u["age"] > min_age]
return sorted(result, key=lambda u: u["name"])
print(query_older_than({"minAge": 28}))
# Параметр подставляется по имени, а не склейкой строк -> нет инъекций
Нажмите «Попробуй сам ▶»: значение приходит как именованный параметр, а не вклеивается в текст запроса. Так Spring защищает от SQL-инъекций.
Частые ошибки
- Путать JPQL и SQL. В JPQL — имена классов и полей; в нативном — таблиц и колонок. Смешивать нельзя.
- Склейка параметров строками. Конкатенация значения в текст запроса открывает SQL-инъекцию. Только
:paramили?. - Нативный запрос там, где хватило бы JPQL. Теряете переносимость без необходимости.
Best practices
- Предпочитайте JPQL ради переносимости; нативный SQL — только когда нужны фишки конкретной СУБД.
- Всегда используйте именованные параметры
:nameс@Param— это безопасно и читаемо. - Сложные запросы покрывайте тестами на реальной базе.
Итог: @Query даёт контроль, когда имени метода мало. JPQL работает над объектной моделью и переносим, нативный SQL — мощнее, но привязан к СУБД. Параметры всегда подставляйте по имени.
Закрепим главное
Ключевое различие, которое стоит закрепить: JPQL мыслит объектами, а нативный SQL — таблицами. JPQL-запрос SELECT u FROM User u Hibernate сам переведёт в SQL под вашу СУБД, поэтому он переносим. Нативный запрос идёт в базу почти дословно, открывая доступ к специфическим возможностям конкретной СУБД ценой привязки к ней. Сначала пробуйте JPQL и переходите на нативный, только когда без фишек базы действительно не обойтись.
Вторая мысль — про безопасность, и она критична. Никогда не склеивайте пользовательские данные в текст запроса конкатенацией строк: это открывает SQL-инъекцию, одну из самых опасных уязвимостей. Всегда используйте именованные параметры :name с @Param — они подставляются через подготовленный запрос, где данные и код запроса строго разделены. Это правило не имеет исключений: даже если данные «кажутся безопасными», параметризуйте их. Привычка параметризовать всё подряд защитит вас задолго до того, как вы задумаетесь об атаке.