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 в большинстве случаев отлично закрывают read‑сценарии приложения.
Если использовать native query везде, вы платите сразу несколькими “налогами”. Во‑первых, вы вручную поддерживаете SQL‑строки, а они не компилируются и не проверяются до рантайма. Во‑вторых, вы увеличиваете связанность с PostgreSQL‑схемой, и запросы становятся менее переносимыми. В‑третьих, команда (или вы через два месяца) начинает читать репозитории как набор разношёрстных SQL‑вставок, а не как аккуратный repository layer.
Поэтому правило дня звучит просто: native query должна появляться там, где она действительно приносит пользу, а не там, где “просто можно”.
Цена native query: сопровождение и переносимость
Когда вы добавляете native query, вы берёте на себя ответственность за кусочек SQL внутри Java‑кода. Сопровождение здесь становится чуть более “DB‑ориентированным”: вы должны помнить имена таблиц, колонок, нюансы join’ов. Если вы переименовали колонку в миграции, то Java‑код не подскажет вам, что вы забыли обновить SQL‑строку. Это всплывёт в рантайме, иногда — только в конкретном редком сценарии.
Есть и вопрос переносимости. JPQL (в идеале) пишет запрос на уровне JPA‑модели и даёт некоторую независимость от конкретной СУБД. Native query — это контракт с конкретной БД и её диалектом. В нашем курсе это нормально, потому что мы и так делаем ставку на PostgreSQL. Но с инженерной точки зрения важно понимать: вы не просто написали запрос, вы “прибили” его гвоздями к конкретной схеме и конкретной СУБД.
И ещё один момент, который студенты часто недооценивают: native query быстро превращает репозиторий в «помойку строк», если не держать дисциплину. Поэтому очень важно, чтобы метод назывался по смыслу (countLowStockItems, findLowStockReport), а не по технике (findAllNative, nativeQuery2). В репозитории должен читаться бизнес‑вопрос, а не способ его реализации.
3. Сценарии: оправдано и вредно
Когда native query оправдана
Есть несколько типов задач, где native SQL — не каприз, а нормальный инженерный выбор. Сразу оговорюсь: мы сейчас говорим именно про read‑сценарии. Запись и массовые изменения — это отдельная история, и её мы сознательно не открываем в этой лекции, чтобы не смешивать инструменты.
Первый класс — это отчётные запросы и сводки, которые естественно формулируются как “посчитать”, “сгруппировать”, “найти топ”, “найти минимальные/максимальные значения”. Иногда JPQL может это выразить, но часто получается либо слишком громоздко, либо неудобно читать, либо начинаются нюансы, где SQL выглядит просто честнее. В mini‑shop это напрямую ложится на отчёты по складу: “сколько товаров ниже порога”, “какие категории чаще всего попадают в low-stock”, “какие товары заканчиваются”.
Второй класс — запросы, которые завязаны на специфические возможности конкретной СУБД. Мы в курсе используем PostgreSQL как truth‑базу, и иногда хочется использовать что-то, что 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”.
Decision‑модель для shop-data-jpa
Чтобы не выбирать инструмент “по настроению”, полезно иметь простую схему выбора. Она не идеальна, но спасает от большинства типовых ошибок. Ниже — именно то, что нам нужно на уровне Junior: не философия, а рабочая памятка.
Сначала вы задаёте себе вопрос: “Это чтение сущности как доменного объекта, или это отчёт/сводка?”. Если это обычное чтение сущности под фильтр, вы почти всегда начинаете с derived query, а если не хватает — переходите к JPQL. Если это отчёт или сводка, вы почти всегда начинаете с projections (чтобы читать только нужные поля), а вот дальше уже смотрите: можно ли выразить это JPQL‑ом читабельно. Если можно — отлично. Если получается плохо или нужен DB‑специфичный приём — вот тогда native query становится кандидатом.
Наглядно это можно представить так:
flowchart TD
%% Выбираем инструмент не по “вкусу”, а по типу read-сценария
A["Нужно read-сценарий"] --> B{"Это обычное чтение entity
или отчёт/сводка?"}
%% Ветка: читаем доменную сущность
B -->|Entity read| C["Derived query"]
C --> D{"Метод читается
и хватает возможностей?"}
D -->|Да| E["Оставляем derived"]
D -->|Нет| F["JPQL + @Query"]
%% Ветка: отчёт/сводка (обычно не нужна целая entity)
B -->|Report / summary| G["Projection (interface/record)"]
G --> H{"JPQL выражает запрос
понятно и коротко?"}
H -->|Да| I["JPQL + projection"]
H -->|Нет / DB-specific| J["Native SQL (точечно)"]
В этой лекции нам важно именно последнее ответвление: native SQL появляется не потому, что “так круче”, а потому что мы честно дошли до точки, где SQL лучше выражает задачу.
Как только это решение принято, остаются уже не философские, а инженерные вопросы: писать запрос на языке схемы БД и заранее понимать, во что вернётся результат. Без этого native query очень быстро превращается в случайный SQL внутри репозитория.
4. Мини‑примеры из mini‑shop
Сейчас мы сделаем два коротких примера, чтобы закрепить идею границы. Они нарочно простые: цель лекции — не научить писать SQL‑строки красиво (это дальше), а научить правильно выбирать.
Первый пример — обычный entity‑ориентированный read‑сценарий каталога. Здесь 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”.
И ещё один момент, который полезно проговорить вслух. Этот пример намеренно делает запрос максимально “скромным”: без сложных join’ов, без хитрой выборки колонок. Чем проще native query, тем меньше цена её сопровождения и тем меньше шанс случайно превратить репозиторий в SQL‑музей.
5. Типичные ошибки при выборе native query
Ошибка №1: выбирать native query «на автомате», потому что “SQL надёжнее”.
Такое мышление часто идёт из опыта без ORM: человек привык, что если написал SQL — значит контролирует результат. Проблема в том, что в Spring Data JPA у вас уже есть инструменты, которые дают читаемость, отделяют запрос от схемных деталей и уменьшают связанность. Native query нужно выбирать не из любви к контролю, а из необходимости.
Ошибка №2: переписывать короткий и читабельный JPQL в SQL без прикладной причины.
Иногда кажется: “JPQL всё равно строка, так лучше уж SQL”. Но JPQL выражает запрос через entity‑модель, а значит, он лучше «встроен» в ваш проектный язык. Если use case хорошо выглядит в JPQL, native SQL чаще всего не улучшает код, а просто переносит вас на уровень ниже.
Ошибка №3: не учитывать зависимость от схемы БД и последствия переименований.
В native SQL вы руками пишете имена таблиц и колонок. Если схема меняется, вы обязаны синхронно обновлять запрос. В учебном проекте это может казаться мелочью, но в реальной команде это быстро превращается в баги “у нас упал отчёт после миграции, но только на проде”.
Ошибка №4: давать методу имя, которое описывает технику, а не смысл.
Название вроде findAllNativeProducts звучит как признание в том, что вы не знаете, что именно ищете. Репозиторий — это не витрина технологий, а слой, который отвечает на бизнес‑вопросы. Хорошее имя фиксирует смысл: countLowStockItems, findLowStockReport, findProductCardRow.
Ошибка №5: воспринимать один удачный отчёт как повод переписать весь querying‑слой в SQL.
Самая частая “эволюционная” ошибка: вы сделали один отличный отчёт native query, он работает, вы довольны, и дальше рука тянется писать так же всё. Итог обычно грустный: репозитории превращаются в набор несвязанных SQL‑строк, а ценность Spring Data JPA начинает исчезать. Native query должна оставаться исключением — ровно настолько, насколько use case этого требует.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ