1. Вступ
Якщо вперше побачити findByStatusAndNameContainingIgnoreCase(...), у новачка реакція зазвичай щира: «А це точно Java, а не заклинання з Гаррі Поттера?». І це нормально. У цьому розділі важливо прибрати «ауру магії»: Spring Data не “вгадує” запит, наче ворожка, а використовує досить суворі правила розбору імʼя методу та будує запит за ними.
Spring Data JPA підтримує підхід derived queries: ви описуєте запит не текстом SQL/JPQL, а імʼям методу, і Spring Data будує потрібний запит за вас. Це працює тому, що Spring Data розуміє структуру репозиторію, знає вашу entity-модель (поля, зв’язки), уміє парсити певні ключові слова в імені методу та перетворювати їх на умову WHERE (а іноді — і на сортування).
Важливий момент: derived query — це не «хак для лінивих». Це спосіб зробити контракт репозиторію читабельним, щоб за однією назвою методу було зрозуміло, які дані ми шукаємо і за якою умовою.
У нас уже є сутності, зв’язки та базові репозиторії. Тепер видно, що репозиторій потрібен не лише для save() і findById(): короткі сценарії читання каталогу можна виразити самим імʼям методу, а потім на цю основу наростити сортування, обмеження вибірки та вибір між Page і Slice. Поки тримаємося в межах derived query: тексту запиту ще немає, але розуміти його сенс і ціну вже потрібно.
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"]
Тут є дві особливо корисні думки.
Перша: 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 підштовхують до правильної об’єктної моделі: навігація йде по полях сутностей, а не по сирих значеннях зовнішнього ключа.
Помилка №5: тягнути дані, коли потрібен лише факт або число.
Якщо вам треба “перевірити, чи існує SKU”, а ви робите findBySku(...) і потім перевіряєте результат, ви тягнете зайві дані та ускладнюєте сервісний код. Для таких сценаріїв краще використовувати existsBy... або countBy... і зробити контракт репозиторію чесним: «мені потрібен факт» або «мені потрібна кількість».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ