@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 — они подставляются через подготовленный запрос, где данные и код запроса строго разделены. Это правило не имеет исключений: даже если данные «кажутся безопасными», параметризуйте их. Привычка параметризовать всё подряд защитит вас задолго до того, как вы задумаетесь об атаке.

Проверьте себя
1. Чем JPQL отличается от нативного SQL в @Query?
AJPQL быстрее во всех случаях
BJPQL оперирует именами классов-сущностей и полей и переносим между БД, а нативный SQL — реальными таблицами и колонками
CЭто одно и то же
DJPQL нельзя использовать в Spring Boot 3
2. Почему нужно использовать именованные параметры (:minAge) вместо склейки значений в строку запроса?
AДля красоты кода
BЧтобы защититься от SQL-инъекций через безопасную подстановку (prepared statement)
CЭто требование компилятора
DЧтобы запрос работал быстрее