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, вы получаете либо ошибку, либо запрос, который логически не делает того, что вы ожидаете. В рамках сегодняшней темы правильный подход простой: параметры для таких запросов считаем обязательными. Если сценарий предполагает «иногда есть фильтр, иногда нет», его нужно проектировать иначе, но это уже отдельная история и отдельный набор инструментов.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ