JavaRush /Курсы /Spring Data JPA /@Query и параметры

@Query и параметры

Spring Data JPA
11 уровень , 2 лекция
Открыта

1. Роль параметров в @Query

Если честно, написать JPQL-строку — это ещё не победа. Победа начинается там, где запрос может пережить жизнь проекта: новые поля, переименование параметров, добавление условий и самое страшное — «коллега открыл репозиторий и понял, что происходит». Параметры — это как подписи на кнопках: без них UI вроде есть, но пользоваться больно.

В простейшем виде запрос можно «сварить» из строк руками, но это путь в мир кавычек, SQL-инъекций (да, даже в JPQL можно устроить себе приключение) и неожиданного поведения на проде. Поэтому в Spring Data JPA нормальный стиль — параметризованные запросы: значения передаются отдельно, а не вплетаются в текст.

С точки зрения нашего проекта shop-data-jpa это выглядит так: репозиторий объявляет метод вроде «найти товары каталога по коду категории и верхней границе цены», а конкретные значения ("electronics", 499.99) приходят как параметры метода. Мы получаем читаемый контракт и уменьшение «магии» в месте, где магии и так хватает.

Ниже снова будут локальные фрагменты методов и репозиториев: нам сейчас важна именно механика параметров, а не полный интерфейс целиком.

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

// Плохая идея: значения впаяны в строку.
// Сегодня мы так не делаем.
//
// Проблема не только в «некрасиво»: такой запрос нельзя нормально переиспользовать
// и он провоцирует «правки на месте» вместо явных параметров.
@Query("""
       select p
       from Product p
       where p.sku = 'ABC-123'
       """)
Optional<Product> findHardcoded();

Запрос вроде бы «работает», но это уже не метод, а мем: его нельзя переиспользовать, нельзя тестировать на разных данных, нельзя нормально рефакторить. Параметры — это способ превратить запрос в инструмент, а не в одноразовую записку на холодильнике.

2. Как выполняется запрос @Query

Когда вы пишете интерфейс репозитория, ощущение бывает странное: «я объявил метод, а реализацию не написал, и всё работает». С @Query это ощущение становится ещё мистичнее: «я ещё и строку запихнул в аннотацию — и оно пошло в базу». Чтобы не воспринимать это как чёрную магию, полезно понимать цепочку событий хотя бы на уровне здравого смысла.

Spring Data, когда поднимает контекст, сканирует репозитории и для каждого метода решает: как его реализовать. Если метод без @Query, он пытается вывести запрос из имени (derived query). Если метод с @Query, он берёт ваш текст JPQL как источник правды и готовит «шаблон запроса», в который потом подставит параметры. Дальше Hibernate (как JPA-провайдер) превращает JPQL в SQL и выполняет его через JDBC-драйвер PostgreSQL. И да, в конце концов всё равно будет «обычный» SQL, просто вы его не писали вручную.

Вот упрощённая схема того, что происходит, когда вы вызываете метод репозитория с @Query:

flowchart TD
    A["Вызываем метод репозитория"] --> B["@Query: JPQL-строка"]
    B --> C["Spring Data: связывает параметры метода и параметры запроса"]
    C --> D["Hibernate: превращает JPQL в SQL"]
    D --> E["JDBC драйвер PostgreSQL: выполняет SQL"]
    E --> F["Результат превращается в объекты и значения"]

И в этой схеме параметры — это центральный момент. Если вы написали :sku в запросе, а привязка параметра расходится по именам или остаётся неявной, на ровном месте получится очень «весёлый» баг: запрос вроде написан, а значение к нему не привязалось. Приложение либо упадёт при старте, либо при первом вызове метода.

3. Именованные параметры и @Param

Именованные параметры — это когда в JPQL вы пишете «дырку» вида :sku, а затем говорите Spring Data: «вот это значение из аргумента метода подставь в параметр sku». Это самый дружелюбный стиль к человеку. Он читается почти как текст: where p.sku = :sku. Даже если вы не фанат баз данных, выглядит достаточно очевидно.

В нашем проекте это особенно удобно, потому что у нас есть доменные поля с понятными именами: sku, orderNumber, customerEmail, category.code. И если в запросе написано :orderNumber, то в методе хочется видеть параметр orderNumber, а не a или x.

Давайте добавим в репозиторий каталога метод, который находит товар по sku. Да, derived query и так могла бы это сделать, но нам важен именно пример связывания параметра.

package com.example.shopdatajpa.catalog.repository;

import com.example.shopdatajpa.catalog.entity.Product;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Именованный параметр в JPQL: ":sku".
    // Это «имя», по которому Spring Data будет связывать аргумент метода со строкой запроса.
    @Query("""
           select p
           from Product p
           where p.sku = :sku
           """)
    // @Param("sku") — явная привязка аргумента метода к именованному параметру ":sku" в JPQL.
    Optional<Product> findBySkuJpql(@Param("sku") String sku);
}

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

Во-первых, @Param берётся из org.springframework.data.repository.query.Param, а не из JPA. Это именно «клей» Spring Data между параметром метода и параметром запроса.

Во-вторых, имя в @Param("sku") должно совпадать с тем, что после двоеточия в запросе (:sku). Совпадать буквально, включая регистр. Компьютер шутки про «я же почти так написал» не понимает.

В-третьих, метод можно назвать коротко и по смыслу. Если у нас в проекте уже есть derived метод findBySku, то findBySkuJpql — временное учебное имя. В реальном коде чаще делают наоборот: оставляют derived findBySku, а @Query используют там, где имя уже не вытягивает сценарий.

Теперь пример чуть более жизненный: запрос на «каталожные товары», где нужно перейти по связи к категории и проверить несколько условий. Здесь именованные параметры начинают реально выигрывать.

package com.example.shopdatajpa.catalog.repository;

import com.example.shopdatajpa.catalog.entity.Product;
import java.math.BigDecimal;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface ProductRepository extends org.springframework.data.jpa.repository.JpaRepository<Product, Long> {

    // Обратите внимание: параметры называем «по домену», чтобы запрос читался как договорённость.
    // Имена :categoryCode и :maxPrice должны совпасть с @Param("...") ниже.
    @Query("""
           select p
           from Product p
           where p.category.code = :categoryCode
             and p.category.active = true
             and p.price <= :maxPrice
           """)
    List<Product> findCatalogProducts(@Param("categoryCode") String categoryCode,
                                      @Param("maxPrice") BigDecimal maxPrice);
}

Тут мы ещё заодно используем Java text block (""" ... """). Это не обязательная магия, а просто способ не превращать запрос в «лапшу» из строк и плюсов. Для новичков это прям подарок: JPQL выглядит почти как нормальный многострочный текст.

Самое важное — не пытаться «сэкономить» на @Param, надеясь, что Spring сам догадается по имени аргумента метода. Иногда действительно догадается (если проект компилируется с флагом -parameters и имена параметров доступны в рантайме), а иногда нет. А ещё хуже: на одной машине у одного разработчика «работает», а у другого — «почему-то нет». Поэтому в учебном и командном коде @Param — честная и стабильная привычка.

Если хочется понять, почему это бывает нестабильно, вот пример минимальной настройки Gradle, которая включает сохранение имён параметров (это общий Java-нюанс, не только про Spring Data):

tasks.withType<JavaCompile>().configureEach {
    // Сохраняем имена параметров методов в байткоде (нужно, чтобы фреймворки могли их читать в рантайме).
    // Даже с этим флагом @Param обычно стоит оставлять: он делает связывание параметров явным.
    options.compilerArgs.add("-parameters")
}

Но даже с этим флагом @Param остаётся хорошим стилем, потому что делает связь параметров явной в коде. Явность в data-layer обычно окупается.

4. Позиционные параметры ?1, ?2

Позиционные параметры — это второй стиль, который поддерживает JPQL в Spring Data JPA. Вместо :maxPrice вы пишете ?2, а потом передаёте второй аргумент метода как значение для этого параметра. Стиль рабочий, но более хрупкий: читателю нужно держать в голове, что такое ?1 и ?2, и как они соотносятся с сигнатурой метода.

Иногда позиционные параметры встречаются в старом коде и в примерах из интернета (особенно «копипаста из 2015-го»). Мы должны уметь их читать и понимать, но использовать их как default — не самая лучшая идея, если запрос не совсем тривиальный.

Вот тот же запрос «каталожные товары», но позиционным стилем:

package com.example.shopdatajpa.catalog.repository;

import com.example.shopdatajpa.catalog.entity.Product;
import java.math.BigDecimal;
import java.util.List;
import org.springframework.data.jpa.repository.Query;

public interface ProductRepository extends org.springframework.data.jpa.repository.JpaRepository<Product, Long> {

    // Позиционные параметры: ?1 соответствует первому аргументу метода, ?2 — второму.
    // Важно: при перестановке аргументов метода этот запрос легко «сломать логически», не сломав компиляцию.
    @Query("""
           select p
           from Product p
           where p.category.code = ?1
             and p.price <= ?2
           """)
    List<Product> findCatalogProductsByPosition(String categoryCode, BigDecimal maxPrice);
}

Формально всё нормально: ?1 — первый параметр метода, ?2 — второй. Но дальше начинается человеческий фактор. Через месяц вы добавите третий параметр (например, status), переставите их местами в методе, и вам придётся очень внимательно перепроверять, что ?1, ?2, ?3 остались корректны. Это то самое место, где компилятор не спасёт, а баг будет выглядеть как «данные странно фильтруются».

Чтобы зафиксировать разницу, полезна маленькая табличка сравнения:

Стиль параметров Как выглядит в JPQL Как выглядит в методе Основной плюс Основной минус
Именованный :sku, :maxPrice @Param("sku") String sku читабельность и устойчивость к перестановкам нужно дисциплинированно совпадать именами
Позиционный ?1, ?2 String sku, BigDecimal maxPrice коротко, быстро накидать легко сломать при рефакторинге сигнатуры

Важно запомнить одно практическое правило: если параметров больше одного и запрос не «на три слова», именованные параметры обычно выигрывают. И выигрывают не у Spring, а у людей, которые будут читать код.

5. Дисциплина параметров в запросах

Если вы работали с базой хоть немного, вы знаете: запросы имеют неприятное свойство жить дольше, чем любые обещания «я потом отрефакторю». Поэтому именно здесь полезно чуть-чуть дисциплины, чтобы ваш репозиторий не превратился в музей случайных строк.

Самое первое правило звучит скучно, но спасает: не смешивайте стили параметров в одном запросе. То есть либо все параметры именованные (:sku, :maxPrice), либо все позиционные (?1, ?2). Смешивание почти всегда заканчивается ошибками и лишними вопросами к себе же: «а почему вот здесь :code, а тут внезапно ?2?».

Второе правило — давайте параметрам имена «по домену», а не «по настроению». Если у нас есть categoryCode, то так и называем. Если есть orderNumber, то не надо сокращать до n. Сокращения в запросах — это как комментарий «сделано на коленке»: формально работает, но читать неприятно.

Третье правило — делайте запрос визуально читаемым. Java text blocks здесь очень помогают. Особенно когда условий два-три и больше.

Вот пример запроса, где мы ищем товары по части имени (через like) и ограничиваем цену. Смысл не в самом like, а в том, как выглядят параметры и как легко глазами сопоставить их с аргументами метода.

package com.example.shopdatajpa.catalog.repository;

import com.example.shopdatajpa.catalog.entity.Product;
import java.math.BigDecimal;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface ProductRepository extends org.springframework.data.jpa.repository.JpaRepository<Product, Long> {

    // namePart — это «кусок строки», который мы оборачиваем в %...% прямо в JPQL.
    // maxPrice — верхняя граница цены (параметр обязателен, иначе условие <= станет проблемным).
    @Query("""
           select p
           from Product p
           where lower(p.name) like lower(concat('%', :namePart, '%'))
             and p.price <= :maxPrice
           order by p.name
           """)
    // Важно: значения передаются отдельно от текста запроса — это и безопаснее, и понятнее при чтении.
    List<Product> searchByNameAndMaxPrice(@Param("namePart") String namePart,
                                         @Param("maxPrice") BigDecimal maxPrice);
}

Здесь вы прямо видите «склейку»: :namePart связан с @Param("namePart"), а :maxPrice — с @Param("maxPrice"). Даже если вы не помните синтаксис наизусть, логика считывается.

И ещё один маленький, но важный нюанс: не бойтесь использовать @Param даже там, где кажется «и так понятно». Парадоксально, но в запросах явность экономит больше времени, чем занимает.

6. Типичные ошибки параметров @Query

Ошибка №1: параметр в JPQL называется одним словом, а в @Param — другим.
Самый частый сценарий выглядит так: вы пишете запрос ... where p.sku = :sku, а в методе ставите @Param("productSku") String sku. Spring Data не читает мысли и не понимает, что «productSku — это же почти sku». В итоге при старте приложения или при вызове метода вы получите ошибку, что параметр не связан. Лечится это банально: имя после двоеточия и имя в @Param("...") должны совпадать буквально.

Ошибка №2: забыли двоеточие у именованного параметра.
Если написать where p.sku = sku вместо where p.sku = :sku, JPQL-парсер попробует трактовать sku как идентификатор (например, как алиас или поле), и дальше начнётся путаница, которая выглядит как «какая-то странная синтаксическая ошибка». Тут помогает простая привычка: глазами искать двоеточия у всех параметров и держать параметры визуально «одинаковыми» по стилю.

Ошибка №3: смешали именованные и позиционные параметры в одном запросе.
Технически это почти всегда приводит к ошибке, а методически — к нервным тикaм у тех, кто это читает. Например, ... where p.category.code = :code and p.price <= ?2. Даже если это вдруг заведётся в каком-то окружении, дальше оно начнёт ломаться при минимальном изменении сигнатуры. Лучше выбрать один стиль на метод и придерживаться его.

Ошибка №4: позиционные параметры сломались после рефакторинга сигнатуры метода.
С позиционным стилем легко сделать так: вы добавили параметр первым (status), а запрос всё ещё ожидает, что ?1 — это categoryCode. В результате данные фильтруются «не так», но формально код компилируется и тесты могут не покрыть этот случай. Если уж вы используете позиционные параметры, относитесь к изменению порядка аргументов как к опасной операции: после неё запрос надо перечитать целиком. В большинстве случаев проще перейти на именованные параметры и забыть об этой категории ошибок.

Ошибка №5: передали null в параметр и ожидали, что запрос «как-нибудь сам разберётся».
JPQL не воспринимает null как «условие не применять». Если у вас написано p.price <= :maxPrice, а maxPrice равен null, вы получаете либо ошибку, либо запрос, который логически не делает того, что вы ожидаете. В рамках сегодняшней темы правильный подход простой: параметры для таких запросов считаем обязательными. Если сценарий предполагает «иногда есть фильтр, иногда нет», его нужно проектировать иначе, но это уже отдельная история и отдельный набор инструментов.

1
Задача
Spring Data JPA, 11 уровень, 2 лекция
Недоступна
Именованные параметры в `@Query`
Именованные параметры в `@Query`
1
Задача
Spring Data JPA, 11 уровень, 2 лекция
Недоступна
Позиционные параметры `?1` и `?2`
Позиционные параметры `?1` и `?2`
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ