JPQL и @Query: предел derived queries

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

1. Derived queries на старте

Derived queries — это как автодополнение в IDE: пока задача простая, оно экономит время и делает код приятнее. Вы пишете метод вроде findBySku(...) или findByStatus(...), и Spring Data JPA сам строит запрос, отправляет SQL в базу и возвращает вам результат. Для новичка это идеальный вход: меньше синтаксиса, больше смысла.

Начнём с честного примера: короткий метод, одно условие, легко читается и в репозитории, и в сервисе.

Ниже я специально буду брать локальные куски ProductRepository: так проще увидеть сам предел derived queries, не таская за собой весь интерфейс целиком.

package com.example.shopdatajpa.catalog.repository;

import com.example.shopdatajpa.catalog.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

// Репозиторий Spring Data JPA: базовые CRUD-операции уже есть в JpaRepository
public interface ProductRepository extends JpaRepository<Product, Long> {

    // Derived query: Spring Data сам построит запрос по имени метода (по полю sku)
    Optional<Product> findBySku(String sku);
}

Здесь имя метода — короткое, почти разговорное. В сервисном коде это выглядит ещё лучше: productRepository.findBySku(sku). Даже если вы на секунду забыли детали, мозг читает это без «дешифровки».

И в этом главный плюс derived queries: они работают идеально, пока запрос можно выразить коротко и очевидно, и при этом имя метода остаётся человеческим, а не превращается в «метод-колбасу».

2. Длинные имена методов

Как только запрос становится сложнее, derived query начинает требовать, чтобы вы «прописали» условия прямо в имени. Сначала это выглядит терпимо, затем — как пароль от Wi‑Fi, который вам диктуют по телефону: вроде всё логично, но хочется переспросить, где тут дефисы и заглавные буквы.

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

import com.example.shopdatajpa.catalog.entity.Product;
import com.example.shopdatajpa.catalog.entity.ProductStatus;
import org.springframework.data.jpa.repository.JpaRepository;

import java.math.BigDecimal;
import java.util.List;

// Репозиторий для чтения каталога: пока ещё derived query, но имя уже растёт
public interface ProductRepository extends JpaRepository<Product, Long> {

    // Важно: все условия (category.code, status, price <=) зашиты прямо в имя метода
    List<Product> findByCategory_CodeAndStatusAndPriceLessThanEqual(
            String categoryCode, ProductStatus status, BigDecimal maxPrice);
}

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

В этот момент важно поймать мысль: derived queries не «сломались» и не стали плохими. Просто они по природе своей переезжают из зоны «удобно» в зону «слишком много деталей в имени». И это нормальный инженерный этап взросления проекта.

3. Переходы по связям в derived queries

В нашем mini-shop сущности связаны: у Product есть Category, у OrderItem есть Product, у заказа есть позиции. Это хорошо — так модель становится ближе к реальности. Но есть побочный эффект: в запросах мы начинаем фильтровать не только по полям Product, но и по полям связанных сущностей, и derived query заставляет «выписывать» это в имени.

Например, вы хотите: «дай товары из активных категорий». В объектной модели это выглядит естественно, но в derived query начинает расти «дорожка» из имён:

import com.example.shopdatajpa.catalog.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

// Пример перехода по связи: category -> active
public interface ProductRepository extends JpaRepository<Product, Long> {

    // Category_ActiveTrue означает фильтрацию по полю active у связанной сущности category
    List<Product> findByCategory_ActiveTrue();
}

А теперь добавим ещё один шаг: категория активна, код категории такой-то, товар активен, цена не больше, имя содержит подстроку. В бизнес-словах это один сценарий. В derived query — уже литературное произведение:

import com.example.shopdatajpa.catalog.entity.Product;
import com.example.shopdatajpa.catalog.entity.ProductStatus;
import org.springframework.data.jpa.repository.JpaRepository;

import java.math.BigDecimal;
import java.util.List;

// Пример «глубокого» derived query: много условий + несколько переходов по связям
public interface ProductRepository extends JpaRepository<Product, Long> {

    // Чем больше условий, тем больше риск превратить имя метода в «строку запроса без пробелов»
    List<Product> findByCategory_CodeAndCategory_ActiveTrueAndStatusAndPriceLessThanEqualAndNameContainingIgnoreCase(
            String categoryCode, ProductStatus status, BigDecimal maxPrice, String namePart);
}

Сейчас самое время честно спросить себя: это вы репозиторий проектируете, или репозиторий проектирует вас?

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

4. Комбинаторный взрыв фильтров

Есть отдельный класс боли, который почти неизбежно приходит в любой живой проект: фильтры бывают не только «много условий», но и «часть условий опциональна». То есть сегодня пользователь указал только status, завтра status + category, послезавтра status + price range, и так далее. Если пытаться делать это чисто derived queries, репозиторий начинает пухнуть.

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

Вот типичная «лестница» вариантов, которая выглядит невинно, пока вы не понимаете, что это только начало:

import com.example.shopdatajpa.catalog.entity.Product;
import com.example.shopdatajpa.catalog.entity.ProductStatus;
import org.springframework.data.jpa.repository.JpaRepository;

import java.math.BigDecimal;
import java.util.List;

// Репозиторий начинает «обрастать» комбинациями фильтров
public interface ProductRepository extends JpaRepository<Product, Long> {

    // Только по статусу
    List<Product> findByStatus(ProductStatus status);

    // Статус + категория
    List<Product> findByStatusAndCategory_Code(ProductStatus status, String categoryCode);

    // Статус + диапазон цен
    List<Product> findByStatusAndPriceBetween(ProductStatus status, BigDecimal min, BigDecimal max);

    // Статус + категория + диапазон цен (и дальше этот список обычно начинает расти)
    List<Product> findByStatusAndCategory_CodeAndPriceBetween(
            ProductStatus status, String categoryCode, BigDecimal min, BigDecimal max);
}

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

В какой-то момент вы понимаете, что вы не проектируете API, а просто пытаетесь успеть за хаотичным «взрывом вариантов». И это тот момент, где derived queries начинают уже не помогать, а создавать шум.

5. Когда сервис страдает от имён

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

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

// Вызов выглядит как «формула», и её приходится читать прямо в сервисе
var products = productRepository
        .findByCategory_CodeAndCategory_ActiveTrueAndStatusAndPriceLessThanEqualAndNameContainingIgnoreCase(
                categoryCode, status, maxPrice, namePart);

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

// Вызов выглядит как сценарий: «получи товары каталога»
var products = productRepository.findCatalogProducts(categoryCode, status, maxPrice, namePart);

Во втором случае сервис читается человечески: «получи товары каталога». В первом — «получи... э-э-э... подожди, я дочитаю этот метод до конца». И вот это — реальная инженерная цена. Не производительность, не SQL, а банально читаемость кода.

6. Признаки: пора на явный запрос

Очень хочется иметь одну магическую цифру вроде «после трёх условий всегда переходите на JPQL». В реальности так не работает: иногда и пять условий читаются нормально, а иногда два условия уже выглядят странно, если они идут по нескольким связям и требуют хитрой логики. Но практичные признаки всё же есть — их можно воспринимать как «инженерный тест на здравый смысл».

Ниже — небольшая таблица, которая помогает принять решение без философии и религии (derived queries vs ручные запросы), а просто по признакам поддерживаемости:

Симптом в коде Что это значит Что обычно делают дальше
Имя метода тяжело прочитать вслух без паузы и дыхательных упражнений Метод стал хранить формулу запроса вместо сценария Выносят формулу в явный текст запроса, а имя оставляют про сценарий
В имени появляются несколько переходов по связям (Category, потом ещё что-то) Запрос стал «графовым» и плохо выражается именем Выбирают запрос, который можно красиво отформатировать и поддерживать
Репозиторий растёт из-за комбинаций вариантов (то так, то иначе) Начинается комбинаторный взрыв методов Стараются описывать сценарии иначе, чтобы не плодить методы
В code review приходится разбирать метод по частям Читаемость потеряна, поддержка дорожает В команде договорятся о стиле: простое — derived, сложное — явный запрос
В сервисе вызов репозитория выглядит как «строка запроса без пробелов» Техническая деталь пролезла в бизнес-код Сдвигают детали отбора ближе к репозиторию и делают их читабельными

Здесь важная мысль: «пора» — это не про то, что derived queries неправильные. Это про то, что ваш репозиторий — тоже продукт для чтения людьми. И если люди перестают понимать код быстро, то это уже технический долг, даже если всё идеально работает на локальной базе.

7. Гибридный подход: derived + @Query

Очень типичная ошибка новичка — увидеть, что derived queries могут стать неудобными, и резко «переписать всё» на что-то другое. Это почти всегда вредно. Правильная стратегия взросления — гибридная: вы оставляете derived queries там, где они прекрасны, и перестаёте мучить их там, где они уже начинают мешать.

Например, findBySku остаётся абсолютно нормальным, потому что это коротко, ясно и отражает простой сценарий:

import com.example.shopdatajpa.catalog.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

// Простые derived queries по-прежнему уместны и читаемы
public interface ProductRepository extends JpaRepository<Product, Long> {

    // Сценарий «найти товар по SKU» — коротко и понятно
    Optional<Product> findBySku(String sku);
}

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

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

import java.math.BigDecimal;
import java.util.List;

// Гибридный стиль: имя метода — про сценарий, а условия — в JPQL
public interface ProductRepository extends JpaRepository<Product, Long> {

    // JPQL (не SQL): работаем с сущностью Product и её полями/связями, а не с таблицами
    // Привязку параметров лучше делать явно, чтобы запрос не зависел от совпадения имён аргументов
    @Query("select p from Product p where p.category.code = :code and p.status = :status and p.price <= :maxPrice")
    List<Product> findCatalogProducts(@Param("code") String code,
                                      @Param("status") ProductStatus status,
                                      @Param("maxPrice") BigDecimal maxPrice);
}

Здесь нам важнее другое: метод снова выглядит как API, а не как сериализованный запрос. Параметры привязаны явно через @Param, поэтому запрос не зависит от совпадения имён аргументов и настроек компиляции. Репозиторий становится читабельнее, сервис становится читабельнее, и проект в целом начинает выглядеть как система, а не как сборник «заклинаний из имён методов».

8. Типичные ошибки при работе с derived queries

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

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

Ошибка №2: терпеть «методы-колбасы» просто потому, что Spring Data так умеет.
Spring Data действительно умеет многое, но «уметь» и «стоить того» — разные вещи. Если метод читается хуже, чем обычный запрос, значит вы платите читаемостью за автоматизацию. В реальных командах читаемость — не роскошь, а скорость разработки: чем быстрее человек понимает код, тем меньше случайных багов.

Ошибка №3: называть метод перечислением условий вместо названия сценария.
Название вроде findByCategory_CodeAndStatusAndPriceLessThanEqual честно описывает условия, но оно не описывает смысл. Это часто приводит к тому, что сервисный код начинает «говорить на языке фильтров», а не на языке бизнеса. Когда вы видите, что имя метода стало формулой, полезно остановиться и спросить: а как называется этот сценарий по-человечески?

Ошибка №4: плодить похожие методы под комбинации параметров, не замечая взрыва вариантов.
Первые четыре метода выглядят нормально, но дальше начинается снежный ком: завтра появится createdAt, потом manufacturer, потом «только товары в наличии»… и репозиторий превращается в список вариаций. На раннем этапе полезно хотя бы замечать этот тренд: если вы начали писать «ещё один метод почти такой же, но с одним дополнительным условием», это уже сигнал, что derived queries начинают выходить за зону комфорта.

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

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