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
У нашому міні-магазині сутності повʼязані: у 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 + діапазон цін тощо. Якщо намагатися робити це суто 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-запитів.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ