JavaRush /Курси /Spring Data JPA /Native query: суть

Native query: суть

Spring Data JPA
Рівень 13 , Лекція 0
Відкрита

1. Підстави писати SQL вручну

Зазвичай бажання написати native SQL виникає не з любові до страждань, а з практики. Ви вже знаєте derived queries, JPQL і projections, але іноді запит починає поводитися дивно: то агрегати, то хитре сортування, то звіт, де потрібно рахувати й групувати. У такі моменти здається, що простіше чесно сказати базі: «Дай ось це саме так», — без спроб вмовити JPQL бути тим, чим він не є.

Важливо одразу зафіксувати психологічну пастку. Native query — не ознака того, що ви стали “senior” або що тепер “пишете по-справжньому”. Це як викрутка з насадками: іноді потрібна пласка, іноді хрестова, а іноді взагалі шестигранник. Але якщо ви почнете всім підряд крутити шестигранником, гвинти страждатимуть… і ви теж.

У межах нашого mini‑shop (shop-data-jpa) типовий момент, коли хочеться SQL вручну, — це звітні читання за залишками й товарами. Наприклад, швидко отримати список товарів із низьким залишком (low-stock report) або просто порахувати, скільки таких позицій є в базі.

2. Native query: суть і ціна

Що таке native query

Коли ми говоримо «native query» у Spring Data JPA, то маємо на увазі дуже конкретну річ: метод репозиторію, який виконує прямий SQL — тобто SQL саме для конкретної БД, а не JPQL. Технічно це найчастіше виглядає так: @Query(..., nativeQuery = true).

Принципова різниця тут не в «іншому синтаксисі рядка», а в тому, що змінюється мова мислення. У JPQL ви звертаєтеся до ORM мовою entity-моделі: Product, p.category, p.status. У native SQL ви говорите з базою мовою таблиць і колонок: product, category_id, status. Тобто ви перестаєте спиратися на об’єктну модель як на «словник» запиту й починаєте безпосередньо спиратися на схему БД.

І ось тут починається головна ціна native query. JPQL теж рядок, і він теж може зламатися, але він прив’язаний до вашої Java-моделі й зазвичай дружніший до рефакторингу сутностей. Native SQL жорстко прив’язаний до імен таблиць і колонок, а отже, будь-яка зміна схеми або naming strategy може боляче вдарити по запиту.

Native query як виняток

У навчальних проєктах (і в реальних теж) є небезпечна стадія: «мені сподобалося, як швидко вийшло на SQL». Після першого вдалого запиту хочеться переписати все на native. Це приблизно як після першого вдалого тренування вирішити, що тепер ви — професійний спортсмен, і далі можна харчуватися тільки протеїном і гордістю.

Але Spring Data JPA і Hibernate загалом дають нам цінність саме в тому, що вони знімають рутину навколо типових операцій і дають змогу тримати код ближче до доменної моделі. Derived queries, JPQL і projections у більшості випадків чудово закривають сценарії читання застосунку.

Якщо використовувати native query всюди, ви платите одразу кількома “податками”. По-перше, ви вручну підтримуєте SQL-рядки, а вони не компілюються й не перевіряються до runtime. По-друге, ви збільшуєте пов’язаність із PostgreSQL-схемою, і запити стають менш переносними. По-третє, команда — або ви через два місяці — починає читати репозиторії як набір різношерстих SQL-вставок, а не як акуратний рівень репозиторіїв.

Тому правило дня звучить просто: native query має з’являтися там, де вона справді приносить користь, а не там, де «просто можна».

Ціна native query: супровід і переносність

Коли ви додаєте native query, ви берете на себе відповідальність за шматок SQL усередині Java-коду. Супровід тут стає трохи більш орієнтованим на БД: ви повинні пам’ятати імена таблиць, колонок, нюанси з’єднань. Якщо ви перейменували колонку в міграції, Java-код не підкаже вам, що ви забули оновити SQL-рядок. Це спливе в runtime, іноді — лише в конкретному рідкісному сценарії.

Є і питання переносності. JPQL (в ідеалі) пише запит на рівні JPA-моделі й дає певну незалежність від конкретної СУБД. Native query — це контракт із конкретною БД та її діалектом. У нашому курсі це нормально, тому що ми й так робимо ставку на PostgreSQL. Але з інженерного погляду важливо розуміти: ви не просто написали запит, ви “прибили” його цвяхами до конкретної схеми й конкретної СУБД.

І ще один момент, який студенти часто недооцінюють: native query швидко перетворює репозиторій на «смітник рядків», якщо не тримати дисципліну. Тому дуже важливо, щоб метод називався за змістом (countLowStockItems, findLowStockReport), а не за технікою (findAllNative, nativeQuery2). У репозиторії має читатися бізнес-питання, а не спосіб його реалізації.

3. Сценарії: виправдано й шкідливо

Коли native query виправдана

Є кілька типів задач, де native SQL — не примха, а нормальний інженерний вибір. Одразу зауважу: зараз ми говоримо саме про сценарії читання. Запис і масові зміни — це окрема історія, і ми свідомо не відкриваємо її в цій лекції, щоб не змішувати інструменти.

Перший клас — це звітні запити й зведення, які природно формулюються як «порахувати», «згрупувати», «знайти топ», «знайти мінімальні/максимальні значення». Іноді JPQL може це виразити, але часто виходить або занадто громіздко, або незручно читати, або починаються нюанси, де SQL виглядає просто чесніше. У mini‑shop це безпосередньо лягає на звіти зі складу: «скільки товарів нижче порога», «які категорії найчастіше потрапляють у low-stock», «які товари закінчуються».

Другий клас — запити, які зав’язані на специфічні можливості конкретної СУБД. Ми в курсі використовуємо PostgreSQL як базу-джерело істини, і іноді хочеться застосувати щось, що JPQL не виражає красиво або взагалі не знає. Приклади я не розгортатиму в синтаксис (це вже буде про «як писати»), але ідея така: якщо запит стає орієнтованим на PostgreSQL, то native SQL чесніший, ніж спроба змусити JPQL виглядати як SQL.

Третій клас — випадки, коли потрібно працювати з об’єктами схеми, які не ідеально лягають на entity-модель. Наприклад, якщо ви читаєте з view або зі «службової» таблиці, яку не хочеться мапити як entity, тому що вона не є частиною доменної моделі. У нашому навчальному проєкті ми намагаємося не роздувати схему, але як інженерний принцип це знати корисно: іноді не потрібно перетворювати все на entity заради одного звіту.

І, нарешті, іноді native query виправдана просто тому, що SQL-варіант виходить набагато простішим і зрозумілішим, ніж JPQL-варіант. Але тут є тонкий момент: «зрозуміліший» — це не «мені звичніший SQL», а «будь-який читач репозиторію без танців з бубном зрозуміє, що вибирається і навіщо».

Коли native query зайва

Тепер чесно розберімо зворотний бік. Є дуже типові ситуації, де native query виглядає «серйозно», але фактично є погіршенням.

Якщо ваш сценарій — це звичайне читання сутності за простим фільтром, де derived query читається нормально, то native SQL не додає цінності. Наприклад, знайти продукти за статусом і підрядком назви, відсортувати — це класична територія derived queries і Pageable. У такому місці native запит найчастіше просто робить код крихкішим: ви прив’язуєтеся до колонок, починаєте вручну писати lower(...)/ilike/інші деталі, які до цього були нормально вирішені на рівні Spring Data.

Якщо сценарій чудово виражається коротким JPQL, то native query може перетворитися на «переписування заради переписування». Ви не отримуєте «додаткової потужності», зате отримуєте нижчий рівень абстракції й більше точок, де можна помилитися. Це схоже на ситуацію, коли ви замість List.of("a", "b") починаєте вручну створювати ArrayList, додавати елементи й пишатися, що «все під контролем». Так, під контролем, але навіщо?

Ще один частий анти-випадок — повертати entity з native query там, де вам потрібен короткий ряд звіту. У звітах зазвичай не потрібна повна Product з усіма полями й зв’язками. Набагато правильніше мислити «мені потрібен ряд звіту» й повертати projection або скалярне значення. Але деталі формату результату ми розбиратимемо окремою лекцією цього дня, тому поки просто фіксуємо думку: native query найчастіше виправдана саме для звітного читання, а не для “завантажити entity тим самим способом, тільки на SQL”.

Модель ухвалення рішення для shop-data-jpa

Щоб не вибирати інструмент «за настроєм», корисно мати просту схему вибору. Вона не ідеальна, але рятує від більшості типових помилок. Нижче — саме те, що нам потрібно на рівні Junior: не філософія, а робоча пам’ятка.

Спочатку ви ставите собі запитання: «Це читання сутності як доменного об’єкта чи звіт/зведення?». Якщо це звичайне читання сутності під фільтр, ви майже завжди починаєте з derived query, а якщо не вистачає — переходите до JPQL. Якщо це звіт або зведення, ви майже завжди починаєте з projections, щоб читати лише потрібні поля, а вже далі дивитеся: чи можна виразити це в JPQL читабельно. Якщо можна — чудово. Якщо виходить погано або потрібен прийом, специфічний для БД, ось тоді native query стає кандидатом.

Наочно це можна подати так:

flowchart TD
    %% Обираємо інструмент не за «смаком», а за типом сценарію читання
    A["Потрібен сценарій читання"] --> B{"Це звичайне читання сутності
чи звіт / зведення?"} %% Гілка: читаємо доменну сутність B -->|Читання сутності| C["Derived query"] C --> D{"Метод читається
і можливостей вистачає?"} D -->|Так| E["Залишаємо derived"] D -->|Ні| F["JPQL + @Query"] %% Гілка: звіт / зведення (зазвичай не потрібна ціла entity) B -->|Звіт / зведення| G["Projection (interface/record)"] G --> H{"JPQL виражає запит
зрозуміло й коротко?"} H -->|Так| I["JPQL + projection"] H -->|Ні / специфічний для БД| J["Native SQL (точково)"]

У цій лекції нам важлива саме остання гілка: native SQL з’являється не тому, що «так крутіше», а тому що ми чесно дійшли до точки, де SQL краще виражає задачу.

Щойно це рішення ухвалено, залишаються вже не філософські, а інженерні питання: писати запит мовою схеми БД і заздалегідь розуміти, у що повернеться результат. Без цього native query дуже швидко перетворюється на випадковий SQL усередині репозиторію.

4. Мініприклади з mini‑shop

Зараз ми зробимо два короткі приклади, щоб закріпити межу. Вони навмисно прості: мета лекції — не навчити вас красиво писати SQL-рядки (це далі), а навчити правильно вибирати.

Перший приклад — звичайний сценарій читання сутності каталогу. Тут native SQL зазвичай не потрібен: ми читаємо продукти за статусом і шматочком назви. Це типова задача для derived queries.

package com.example.shopdatajpa.catalog.repository;

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

import java.util.List;

/**
 * Репозиторій «каталогу»: тут ми читаємо доменну сутність Product.
 * У таких сценаріях зазвичай достатньо derived queries.
 */
public interface ProductRepository extends JpaRepository<Product, Long> {

    // Spring Data «зібере» запит за назвою методу:
    // - фільтр за статусом
    // - пошук підрядка в назві
    // - IgnoreCase: без ручного lower(...)/ilike в SQL
    List<Product> findByStatusAndNameContainingIgnoreCase(ProductStatus status, String text);
}

Тут добре те, що метод читається як бізнес-питання. І він живе у світі entity-моделі: ви думаєте про ProductStatus і name, а не про колонку status і функції SQL-діалекту.

Другий приклад — запит, який звучить як чистий звіт: «Скільки позицій складу мають доступний залишок нижче за поріг?». Це спеціально найменший зріз того ж low-stock сценарію: тут нам потрібен не список товарів, а один факт про нього. Тут native SQL може бути хорошим кандидатом, тому що результат — простий scalar (long), а запит мислиться таблицею залишків.

package com.example.shopdatajpa.inventory.repository;

import com.example.shopdatajpa.inventory.entity.StockItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

/**
 * Звітне читання: нам не потрібен список сутностей, потрібна лише скалярна відповідь (count).
 * Тому native query тут виглядає доречно, якщо JPQL виходить гіршим/важчим.
 */
public interface InventoryReportRepository extends JpaRepository<StockItem, Long> {

    @Query(
        // nativeQuery = true означає: це саме SQL конкретної БД, а не JPQL
        value = """
                select count(*)
                from stock_item s
                where s.available_quantity <= ?1
                """,
        // ?1 — позиційний параметр Spring Data JPA: сюди підставиться threshold
        nativeQuery = true
    )
    long countLowStockItems(int threshold);
}

Зверніть увагу на важливий нюанс: метод називається не nativeCountSomething, а за змістом. Тобто навіть якщо завтра ви перепишете його на JPQL або перенесете логіку в іншу форму, контракт методу залишиться тим самим: «порахувати low-stock items».

І ще один момент, який корисно проговорити вголос. Цей приклад навмисно робить запит максимально скромним: без складних з’єднань, без хитрого вибору колонок. Чим простіша native query, тим менша ціна її супроводу й тим менший шанс випадково перетворити репозиторій на SQL-музей.

5. Типові помилки під час вибору native query

Помилка №1: вибирати native query «на автоматі», бо «SQL надійніший».
Таке мислення часто походить із досвіду без ORM: людина звикла, що якщо написала SQL — значить контролює результат. Проблема в тому, що у Spring Data JPA у вас уже є інструменти, які дають читабельність, відокремлюють запит від деталей схеми й зменшують пов’язаність. Native query потрібно обирати не з любові до контролю, а з необхідності.

Помилка №2: переписувати короткий і читабельний JPQL у SQL без практичної причини.
Іноді здається: «JPQL усе одно рядок, тож краще вже SQL». Але JPQL виражає запит через entity-модель, а отже, він краще вбудований у вашу проєктну мову. Якщо сценарій використання добре виглядає в JPQL, native SQL найчастіше не покращує код, а просто переводить вас на нижчий рівень.

Помилка №3: не враховувати залежність від схеми БД і наслідки перейменувань.
У native SQL ви вручну пишете імена таблиць і колонок. Якщо схема змінюється, ви зобов’язані синхронно оновити запит. У навчальному проєкті це може здаватися дрібницею, але в реальній команді це швидко перетворюється на баги на кшталт: «у нас упав звіт після міграції, але тільки на проді».

Помилка №4: давати методу ім’я, яке описує техніку, а не зміст.
Назва на кшталт findAllNativeProducts звучить як визнання в тому, що ви не знаєте, що саме шукаєте. Репозиторій — це не вітрина технологій, а шар, який відповідає на бізнес-питання. Гарна назва фіксує зміст: countLowStockItems, findLowStockReport, findProductCardRow.

Помилка №5: сприймати один вдалий звіт як привід переписати весь шар запитів у SQL.
Найчастіша «еволюційна» помилка: ви зробили один чудовий звіт native query, він працює, ви задоволені, і далі рука тягнеться писати так само все. Підсумок зазвичай сумний: репозиторії перетворюються на набір непов’язаних SQL-рядків, а цінність Spring Data JPA починає зникати. Native query має залишатися винятком — рівно настільки, наскільки цього вимагає сценарій використання.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ