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-запросах.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ