1. Введение
Если в первый раз увидеть findByStatusAndNameContainingIgnoreCase(...), реакция у новичка обычно честная: «А это точно Java, а не заклинание из Гарри Поттера?». И это нормально. В этом разделе важно снять «ауру магии»: Spring Data не “угадывает” запрос в стиле гадалки, а использует довольно строгие правила разбора имени метода и строит запрос по ним.
Spring Data JPA поддерживает подход derived queries: вы описываете запрос не текстом SQL/JPQL, а именем метода, и Spring Data строит нужный запрос за вас. Это работает, потому что Spring Data понимает структуру репозитория, знает вашу entity-модель (поля, связи), умеет парсить определённые ключевые слова в имени метода и превращать их в условие WHERE (и иногда — в сортировку).
Важный момент: derived query — это не «хак для ленивых». Это именно способ сделать контракт репозитория читаемым, чтобы по одному названию метода было понятно: какие данные мы ищем и по какому условию.
У нас уже есть сущности, связи и базовые репозитории. Теперь становится видно, что репозиторий нужен не только для save() и findById(): короткие read-сценарии каталога можно выразить самим названием метода, а потом на эту основу нарастить сортировку, ограничение выборки и выбор между Page и Slice. Пока держимся внутри derived-query territory: текста запроса ещё нет, но понимать его смысл и цену уже обязательно.
2. Derived query: что это такое
Когда вы пишете метод в репозитории и не даёте ему реализации, Spring Data делает две вещи. Сначала она создает прокси-реализацию вашего интерфейса (это мы уже обсуждали в блоке про репозитории). Потом — и это важно сегодня — она смотрит на имя метода и пытается понять, относится ли оно к «словарю» запросных методов. Если относится, она строит объект запроса и будет выполнять его при вызове метода.
Например, такой репозиторий уже является «инструкцией», как искать товары:
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Ищем продукты по значению поля status у сущности Product
List<Product> findByStatus(ProductStatus status);
}
findByStatus — это не просто красивое имя. Это буквально шаблон “найди по полю status”. И это поле — поле Java-сущности, а не колонка таблицы.
Чтобы подчеркнуть разницу, посмотрим на кусочек Product. Допустим, в БД колонка названа created_at, а в Java мы её маппим как createdAt. Derived queries будут оперировать createdAt, потому что они читают именно Java-модель:
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@Entity
public class Product {
// В БД колонка называется created_at, но в Java-модели это свойство createdAt
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
// ...
}
То есть если вы попробуете написать findByCreated_At(...) или findByCreated_at(...), Spring Data честно посмотрит на вас как компилятор на код с ошибкой: «такого свойства у entity нет».
3. Структура имени derived query
Если вы хотите перестать бояться derived queries, полезно запомнить одну простую конструкцию. Имя метода обычно читается как:
префикс + By + имена полей (иногда через связи) + ключевые слова условий.
Это почти как «заказ кофе» в кофейне, где бариста понимает ограниченный словарь. Если вы говорите “латте на миндальном, один шот, без сахара”, вас понимают. Если говорите “сделай мне что-нибудь вайбовое”, вас тоже могут понять, но это уже другой курс (по психологии).
В Spring Data то же самое: есть строгий словарь “слов”, которые имеют смысл.
Вот несколько базовых примеров, которые сегодня нужно воспринимать как грамматику.
Самое простое равенство по полю:
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Равенство: WHERE status = :status
List<Product> findByStatus(ProductStatus status);
}
Проверка существования (очень частый прикладной вопрос в сервисах, особенно для sku):
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Быстрая проверка "существует ли хотя бы одна строка" (без загрузки сущностей)
boolean existsBySku(String sku);
}
Поиск по фрагменту строки и без учёта регистра:
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Поиск по подстроке + игнорирование регистра (обычно превращается в LIKE + lower/upper)
List<Product> findByNameContainingIgnoreCase(String namePart);
}
Здесь Containing и IgnoreCase — это не «английские слова для красоты», а ключевые слова из словаря Spring Data, которые она умеет превращать в условия запроса.
4. Префиксы findBy, existsBy, countBy
Очень важно с самого начала привыкнуть: префикс — это не декорация. Он говорит, какой вопрос задаёт метод. Это особенно полезно, когда вы читаете код сервиса: вы не видите SQL, но видите намерение.
Посмотрим на несколько похожих вопросов к базе, но с разным смыслом.
findBy... отвечает “дай данные”:
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Ожидаем один товар по бизнес-ключу sku, либо отсутствие результата
Optional<Product> findBySku(String sku);
}
existsBy... отвечает “такой товар вообще есть?”:
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Возвращаем только факт существования
boolean existsBySku(String sku);
}
countBy... отвечает “сколько таких?” — полезно для статистики, мониторинга, простых проверок:
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Возвращаем количество подходящих строк
long countByStatus(ProductStatus status);
}
На уровне БД эти методы могут выглядеть похоже (везде будет WHERE sku = ? или WHERE status = ?), но на уровне контракта репозитория это три разных вопроса. Для сервисного слоя это прям спасение: вы не тянете лишние данные, если вам нужно только “да/нет” или число.
И ещё одна инженерная мысль: чем меньше данных вы тащите из БД, тем легче живётся приложению. Память и сеть тоже любят, когда их не насилуют.
5. Property path: проход по связям
В нашем проекте Product связан с Category через ManyToOne. И в реальном коде мы почти никогда не хотим искать товары по “category_id как числу”, потому что это превращает доменную модель в таблицы с айдишниками. Мы хотим мыслить по-человечески: «товары в категории с кодом TEA» или «товары в категории coffee».
И вот здесь появляется понятие property path: Spring Data умеет “пройти” по полям связанных сущностей, если связь описана в entity-модели.
Допустим, у нас есть Category с полем code:
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@Entity
public class Category {
// Этот код будем использовать в запросах через property path: category.code
@Column(nullable = false, unique = true)
private String code;
// ...
}
А в Product есть поле category:
import jakarta.persistence.Entity;
import jakarta.persistence.ManyToOne;
@Entity
public class Product {
// Связь на категорию: по ней Spring Data может "провалиться" дальше к полю code
@ManyToOne(optional = false)
private Category category;
// ...
}
Тогда запрос “найти товары по коду категории” можно выразить так:
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Property path: category.code (а не "category_id" и не имя колонки в БД)
List<Product> findByCategoryCode(String categoryCode);
}
Обратите внимание на имя: CategoryCode — это не “одно поле в Product”, это «пройди по полю category, потом по полю code». Для чтения кода это невероятно приятно: вы буквально видите, что ищете.
И здесь важная дисциплина: вы называете не “category_code”, не “category_id”, и даже не “categoryCode внутри продукта”, если такого поля нет. Вы называете так, как выглядит объектная модель. Spring Data в этот момент играет роль “переводчика”: она принимает объектный язык, а на другой стороне всё равно получается SQL.
6. Цепочка: имя метода → запрос → SQL
Если сильно упростить и нарисовать «как оно происходит», получится примерно такая цепочка. Здесь нет необходимости знать каждую внутреннюю деталь, но важно видеть общую картину, чтобы derived queries перестали быть “черным ящиком”.
flowchart TD
A["Вы пишете метод findByCategoryCode(...)"] --> B["Spring Data парсит имя метода и строит модель запроса"]
B --> C["Строится JPQL/HQL-подобный запрос по entity-модели"]
C --> D["Hibernate генерирует SQL под PostgreSQL"]
D --> E["PostgreSQL выполняет SQL и возвращает строки"]
E --> F["Hibernate маппит строки в 'Product (entity)'"]
Здесь две особенно полезные мысли.
Первая: derived query опирается на entity-модель, поэтому это именно объектный контракт. И именно поэтому мы постоянно повторяем на курсе: mapping — это проектное решение. Плохо замаппили сущности — будете «страдать» даже при красивых методах.
Вторая: в конце всё равно будет SQL. Поэтому при желании вы всегда можете мысленно (а иногда и по SQL-логам) проверить: что реально улетело в БД.
7. Возвращаемый тип как часть контракта
Для новичков это неожиданно, но полезно: Spring Data не просто парсит имя метода. Она ещё и учитывает возвращаемый тип, и это влияет на то, как будет вести себя запрос и как вы будете работать с результатом.
Если вы возвращаете List<Product>, это “дай все подходящие”. Но как только подходящих товаров много, такой метод быстро упирается в размер выборки, и тогда к нему уже добавляют пагинацию.
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Множественный результат: вернётся список сущностей
List<Product> findByStatus(ProductStatus status);
}
Если вы возвращаете boolean, это “мне не нужны данные, мне нужен факт существования”:
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Булевый результат: оптимально для "проверок" в сервисном слое
boolean existsBySku(String sku);
}
Если вы возвращаете long, это “мне нужно число”:
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Количественный результат: вернётся count
long countByCategoryCode(String categoryCode);
}
Если же по смыслу должен прийти максимум один объект, честнее вернуть Optional<Product>, а не List<Product> и не делать вид, будто уникальный lookup может принести пачку сущностей.
Отдельно отмечу типичный “вкусный” момент: когда вам нужно “проверить, можно ли создать товар с таким sku”, намного логичнее и дешевле использовать existsBySku, чем делать findBySku и потом проверять optional.isPresent(). Сервисный слой становится проще, и вы не тянете сущность просто ради булевого ответа.
8. Перевод имени метода в SQL-смысл
На этом этапе полезно тренировать навык «переводчика». Вы не обязаны писать SQL руками, но вы обязаны понимать, какой SQL получается. Иначе вы не сможете отличить “удобно” от “медленно”.
Сделаем маленькую табличку: слева метод, справа — что примерно происходит в базе. Это не 100% точный SQL символ-в-символ (у Hibernate будут алиасы, параметры, join-ы), но смысл будет очень близким.
| Derived query метод | Интуитивный SQL-смысл (приблизительно) |
|---|---|
| findByStatus(status) | SELECT ... FROM product WHERE status = ? |
| existsBySku(sku) | SELECT 1 FROM product WHERE sku = ? LIMIT 1 (или аналогичный “exists”) |
| countByStatus(status) | SELECT count(*) FROM product WHERE status = ? |
| findByCategoryCode(code) | SELECT ... FROM product p JOIN category c ON ... WHERE c.code = ? |
| findByNameContainingIgnoreCase(part) | WHERE lower(name) LIKE lower('%'||?||'%') (идея поиска по фрагменту) |
Почему это важно? Потому что вы начинаете оценивать “цену” запроса. Например, поиск Containing по большому текстовому полю может быть дорогим без индекса (а про индексы мы уже говорили в SQL-блоке курса). Или findByStatus без пагинации может вернуть слишком много данных.
Здесь мы не оптимизируем и не «тюним» запросы (это отдельная дисциплина). Но мы уже сейчас должны удерживать мысль: “запрос существует и он имеет цену”.
9. Пределы удобства derived queries
С derived queries легко переборщить, потому что сначала всё выглядит как праздник. Ты пишешь findByStatus — и это работает. Пишешь findByStatusAndNameContainingIgnoreCase — и это тоже работает. Потом кто-то добавляет “и по цене”, потом “и по категории”, потом “и по дате”, потом “и ещё отсортируй так-то”… и в итоге получается метод-колбаса, который занимает три экрана и ломает веру в человечество.
В прикладном коде derived queries особенно хороши там, где фильтр короткий, понятный и действительно выражает один сценарий чтения. Обычно это один-два критерия, максимум три, и желательно чтобы название читалось с первого взгляда. Как только вы ловите себя на том, что читаете имя метода по слогам, как первоклассник, который встретил слово “синхрофазотрон”, — это сигнал. Сигнал не обязательно «всё плохо», но точно «мы вышли за комфортную зону derived queries».
И ещё одна тонкость: derived query — это часть контракта репозитория. Если контракт стал нечитабельным, сервисы начинают выглядеть так, будто вы разговариваете с базой данных через SMS на кнопочном телефоне: вроде работает, но удовольствие сомнительное.
10. Типичные ошибки
Ошибка №1: использовать имена колонок вместо имён полей entity.
Очень частая ситуация: в базе колонка created_at, и студент пишет findByCreated_at(...) или findByCreatedAtColumn(...). Spring Data так не умеет, потому что она парсит имя метода в терминах Java-модели. Надо писать findByCreatedAt(...), даже если в аннотации @Column(name = "created_at") колонка называется иначе.
Ошибка №2: придумывать «свои» ключевые слова и ждать, что Spring Data их поймёт.
Например, написать findByNameIncludes(...) вместо findByNameContaining(...). Для человека “includes” выглядит логично, но для Spring Data это просто слово, которое она не знает. Derived queries — это не английский язык, а маленький DSL со своим словарём.
Ошибка №3: пытаться запихнуть бизнес-историю в имя метода.
Иногда появляется метод вроде findByActiveStatusAndAvailableQuantityGreaterThanAndCategoryCodeAndNameContainingIgnoreCase(...). Это уже не репозиторий, а роман. Такие методы сложно читать, сложно переиспользовать, и они быстро превращаются в точку боли, потому что каждое новое условие увеличивает “монстра”.
Ошибка №4: путать “property path по связям” с “я храню id-шники”.
Если в модели есть Product.category, то для сценариев каталога лучше мыслить через findByCategoryCode(...), а не через findByCategoryId(...) (если у вас вообще нет поля categoryId). Derived queries подталкивают к правильной объектной модели: навигация идёт по полям сущностей, а не по сырым FK-числам.
Ошибка №5: тянуть данные, когда нужен только факт или число.
Если вам надо “проверить, существует ли SKU”, а вы делаете findBySku(...) и потом проверяете результат, вы тащите лишние данные и усложняете сервисный код. Для таких сценариев лучше использовать existsBy... или countBy... и сделать контракт репозитория честным: «мне нужен факт» или «мне нужно количество».
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ