1. nativeQuery = true: переключатель режима
Когда вы впервые видите @Query(..., nativeQuery = true), кажется, что это просто «ещё одна галочка» у аннотации. На практике это переключатель, который меняет язык, правила, и даже то, какие ошибки вы будете ловить. В JPQL Hibernate (и JPA‑провайдер) понимает вашу entity‑модель и сам генерирует SQL. В native SQL вы приносите готовый SQL‑текст и говорите: «Дорогая база данных, вот тебе запрос — исполни как есть». И дальше начинается взрослая жизнь: таблицы, колонки, join по FK, регистр, зарезервированные слова и прочие радости.
Повод писать native SQL мы уже отделили от любопытства и желания “контролировать всё руками”. Теперь важно понять, что именно технически меняется после nativeQuery = true: меняется не только синтаксис строки, а сама опора запроса.
Давайте сразу зафиксируем главное: @Query существует и для JPQL, и для native SQL, но это два разных мира.
| Что сравниваем | JPQL (nativeQuery = false) | Native SQL (nativeQuery = true) |
|---|---|---|
| На чём пишем запрос | Entity, поля, связи | Таблицы, колонки, внешние ключи |
| Кто «переводчик» | Hibernate генерирует SQL | Вы сами написали SQL, перевода нет |
| Насколько запрос привязан к схеме БД | Косвенно (через mapping) | Очень жёстко (по именам таблиц/колонок) |
| Типовая ошибка новичка | «Почему p.category.code не работает?» (в SQL) | «Почему таблицы Product не существует?» |
И важный психологический момент: в native SQL вы не становитесь «круче, чем JPQL». Вы просто выбираете другой инструмент, у которого выше цена сопровождения, но иногда он действительно честнее и удобнее под конкретный read‑вопрос.
2. В JPQL мы думаем объектами: сущность, поле, ассоциация
JPQL часто любят за то, что он звучит как «SQL, но по объектам». И это почти правда — если не забывать слово «почти». В JPQL вас интересует не то, как называется таблица, а то, какая это сущность; не то, как называется колонка, а то, какое это поле; не то, как выглядит FK, а то, как вы навигируете по ассоциации. Это делает запросы устойчивее к “переименовали колонку”, но требует дисциплины в entity‑модели: поля и связи должны быть понятными.
Например, если у нас в mini‑shop есть Product и у него есть связь на Category, то в JPQL вы спокойно пишете «продукты, у которых код категории такой‑то»:
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
interface ProductRepository extends JpaRepository<Product, Long> {
@Query("""
select p
from Product p
where p.category.code = :code
""")
// JPQL: работаем с entity-моделью (Product, category, code), а не с таблицами/колонками
List<Product> findByCategoryCodeJpql(@Param("code") String code);
}
Обратите внимание, как JPQL выглядит «по‑человечески»: Product p, p.category.code. Вы вообще не обязаны помнить, что в БД там есть category_id, что таблица называется product, что колонка code лежит в таблице category. За вас это «разрулит» ORM, потому что у него есть mapping.
И вот здесь скрытая опасность: JPQL иногда настолько комфортен, что кажется, будто SQL‑схема вообще не важна. А потом вы включаете nativeQuery = true — и понимаете, что база данных всё это время никуда не уходила, просто вы с ней разговаривали через переводчика.
3. В native SQL думаем схемой
Native SQL — это момент, когда вы снимаете переводчика и начинаете общаться с базой напрямую. А база данных, как любой честный инженер, не понимает «у продукта есть категория». Она понимает: «в таблице product есть колонка category_id, которая ссылается на category.id». Поэтому первое, что меняется — это словарь.
Если в JPQL вы писали Product и p.category.code, то в SQL вы будете писать product и p.category_id, а затем соединять таблицы через join ... on .... Это звучит сухо, но на самом деле это полезно: вы начинаете видеть реальную структуру данных, а не только красивый объектный фасад.
Вот тот же смысл, но уже на native SQL, и это важно: здесь в запросе нет ни одного Java‑имени, только имена таблиц и колонок:
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
interface ProductRepository {
@Query(
value = """
select p.*
from product p
join category c on c.id = p.category_id
where c.code = :code
""",
nativeQuery = true
)
// Native SQL: работаем с таблицами/колонками (product, category_id, category.code)
// p.* здесь намеренно: так проще корректно замапить результат обратно в Product
List<Product> findByCategoryCodeNative(@Param("code") String code);
}
Поймайте внутренний «щелчок»: в SQL нет p.category.code, потому что SQL не знает про вашу навигацию по объектам. У SQL есть только таблицы, колонки и условия.
И ещё один момент, который новичков часто удивляет: даже если вы написали native SQL, вы всё ещё внутри Spring Data JPA. Это значит, что метод репозитория всё равно будет выполнен через JPA‑инфраструктуру, параметры будут забиндены, а результат будет замаплен обратно в то, что вы указали (Product, projection и т.д.). Просто SQL‑текст теперь не генерируется автоматически — вы принесли его сами.
4. Имена таблиц и колонок для native SQL
Когда студент впервые пишет native SQL в репозитории, у него обычно два источника истины: «кажется, таблица называется…» и «ну в Java поле же availableQuantity, значит колонка… наверное такая же». К сожалению, база данных не умеет «понимать намерение» — она очень буквальна. Если колонка называется available_quantity, а вы написали availableQuantity, то для PostgreSQL это два разных слова, и одно из них просто не существует.
Нормальный способ понять, какие имена использовать в native SQL, — смотреть на JPA mapping и на то, что реально создано в БД. На уровне кода это обычно выражается в явных @Table, @Column, @JoinColumn.
Например, наша сущность StockItem в проекте почти наверняка выглядит примерно так (кусочек, чтобы увидеть naming‑разницу):
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
@Entity
@Table(name = "stock_item") // имя таблицы в БД (snake_case)
class StockItem {
@Column(name = "available_quantity", nullable = false) // имя колонки в БД (snake_case)
private int availableQuantity; // имя поля в Java (camelCase)
}
Здесь видно главное: Java‑поле availableQuantity и SQL‑колонка available_quantity — разные. В JPQL вы писали бы s.availableQuantity, а в native SQL обязаны писать s.available_quantity.
Со связями то же самое. Если Product ссылается на Category, то в базе это обычно FK‑колонка category_id, и mapping это отражает:
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
@ManyToOne(optional = false)
@JoinColumn(name = "category_id", nullable = false) // FK-колонка в таблице product
private Category category; // в Java это объектная ссылка, а в БД — число (category_id)
Из этого кусочка вы вытаскиваете сразу две практические вещи для native SQL. Во‑первых, FK‑колонка действительно называется category_id, и join в SQL вы будете писать по ней. Во‑вторых, если вы когда‑нибудь переименуете @JoinColumn(name = "..."), ваш native SQL сломается, потому что он привязан к этому имени напрямую.
И вот здесь рождается честный вывод: native SQL требует дисциплины в схеме и в именовании. Если в проекте хаос с именами таблиц/колонок, native query превращается в игру «угадай правильную строку». И обычно выигрывает не тот, кто лучше угадывает, а тот, кто быстрее идёт смотреть схему.
5. JPQL и native SQL: перевод смысла
Один и тот же запрос двумя языками
Очень полезное упражнение для мозгов — взять один и тот же смысл и переписать его между JPQL и SQL. Это как перевод с русского на английский: не обязательно делать каждый день, но один раз точно стоит, чтобы почувствовать разницу мышления.
В JPQL мы работаем по модели:
import java.util.List;
import org.springframework.data.jpa.repository.Query;
@Query("""
select p
from Product p
where p.category.code = :code
""")
// JPQL: здесь "category" — это ассоциация в модели, а не category_id в таблице
List<Product> findByCategoryCodeJpql(String code);
А в native SQL мы работаем по схеме:
import java.util.List;
import org.springframework.data.jpa.repository.Query;
@Query(
value = """
select p.*
from product p
join category c on c.id = p.category_id
where c.code = :code
""",
nativeQuery = true
)
// Native SQL: здесь всё держится на физических именах таблиц/колонок и на условии join
List<Product> findByCategoryCodeNative(String code);
Сейчас важно не то, что второй запрос длиннее. Важно, что во втором запросе вы обязаны явно указать «как именно связаны таблицы», то есть join category c on c.id = p.category_id. В JPQL вы этого не видели, потому что ORM уже знает, что Product.category замаплен через @JoinColumn(name = "category_id").
И здесь часто происходит маленькое откровение: native SQL — это не «JPQL, где надо писать названия таблиц». Это другая опора. В JPQL опора — это объектная модель. В SQL опора — это физическая модель данных.
JOIN: «по связи» vs «по условию»
В начале карьеры кажется, что JOIN — это просто «соединить две штуки». Но в мире JPA есть две разные “логики соединения”, и native query заставляет их увидеть. В JPQL join выглядит как «я навигирую по ассоциации», а в SQL join выглядит как «я соединяю по условию FK = PK». И это не просто разный синтаксис — это разная ответственность разработчика.
В JPQL вы пишете так, будто просто гуляете по объектам:
import java.util.List;
import org.springframework.data.jpa.repository.Query;
@Query("""
select p
from Product p
join p.category c
where c.code = :code
""")
// JOIN в JPQL: "p.category" — это уже описанная связь, ORM сам знает, как её соединять
List<Product> findByCategoryCodeJpql(String code);
Тут join “правильный” автоматически, потому что p.category уже замаплен.
В native SQL вы обязаны написать условия соединения сами:
import java.util.List;
import org.springframework.data.jpa.repository.Query;
@Query(
value = """
select p.*
from product p
join category c on c.id = p.category_id
where c.code = :code
""",
nativeQuery = true
)
// JOIN в SQL: вы явно пишете ON, потому что база не знает про ваши entity и их ассоциации
List<Product> findByCategoryCodeNative(String code);
И вот где появляется типовая ошибка новичка: он пишет join category c и забывает on ..., или пишет on c.code = p.category (потому что мозг всё ещё думает объектами). Для базы данных это выглядит как «я не понимаю, что ты хочешь соединить».
С native SQL вы должны чётко помнить, где какой FK лежит. А это, внезапно, полезный навык, потому что в реальной разработке вы всё равно иногда будете читать планы запросов, смотреть ошибки FK‑constraint’ов и разбирать “почему join не работает”. Native query просто ускоряет ваше знакомство с реальностью. Да, слегка грубо. Да, без предупреждения. Но зато честно.
6. Возврат entity из native SQL
Как только этот сдвиг в языке запроса случился, сразу возникает очень практический вопрос: во что именно потом маппить результат. С entity у native SQL есть жёсткая граница: если просите сущность, результат должен быть entity-shaped.
Одна из самых подлых ловушек native query — это мысль: «Ну я же возвращаю Product, значит могу выбрать пару колонок, остальное Hibernate как‑нибудь додумает». Нет. Hibernate — конечно, умный, но не телепат. Если вы говорите: «верни мне Product», то вы должны принести результат, из которого реально можно собрать Product так, как он замаплен.
Самый безопасный вариант для возврата entity из native SQL — выбрать все её колонки. В PostgreSQL и вообще в SQL это часто делают через p.*:
import java.util.List;
import org.springframework.data.jpa.repository.Query;
@Query(
value = """
select p.*
from product p
where p.status = 'ACTIVE'
""",
nativeQuery = true
)
// Возвращаем entity -> выбираем все колонки продукта (p.*), чтобы маппинг был стабильным
List<Product> findActiveNative();
Почему p.* удобнее, чем select *? Потому что если у вас join с другими таблицами, select * вернёт колонки всех таблиц, и у вас начнутся коллизии имён (id есть и там, и там), а mapping станет болезненным. p.* явно говорит: «мне нужен только продукт».
Теперь покажу пример, который выглядит логично для новичка, но на практике часто заканчивается ошибкой или странным поведением:
import java.util.List;
import org.springframework.data.jpa.repository.Query;
@Query(
value = """
select p.id, p.sku
from product p
""",
nativeQuery = true
)
// Частичный SELECT + возврат entity = почти гарантированная проблема маппинга
// Если нужны 2-3 поля, обычно делают projection/DTO, а не возвращают Product
List<Product> brokenPartialProduct();
Этот запрос возвращает только id и sku, а вы просите Spring Data замапить это в Product. На такой incomplete native select нельзя рассчитывать как на корректный результат: обычно это либо runtime‑ошибка маппинга, либо provider-specific unsafe behavior, а не нормальный рабочий паттерн. Если нужны 2–3 поля, лучше сразу возвращать projection или другой read-result.
Правило здесь простое: если возвращаете entity — выбирайте entity‑форму данных. Если вам нужно 2–3 поля — возвращайте не entity. И это не «придирка», это архитектурная гигиена: read‑сценарий должен возвращать именно то, что он читает.
7. Native SQL в репозитории: оформление
Native SQL в репозитории — это как чеснок: в правильной дозировке он полезен, но если переборщить, от вас начнут шарахаться коллеги (а иногда и вы сами, глядя на свой код через месяц). Поэтому тут нужна дисциплина оформления. Не ради красоты, а ради выживаемости проекта.
Первое, что сильно помогает на Java 25, — текстовые блоки. Не склеивайте SQL в одну строку с +, если можно оформить его как многострочный текст. Во‑первых, это легче читать. Во‑вторых, легче сравнивать с тем, что вы пробуете в psql/IDE. В‑третьих, меньше шанс, что вы забудете пробел и получите ...fromproduct... (а потом будете минут пять смотреть на запрос, не видя пропущенный пробел — классика жанра).
Пример “по‑человечески”, с нормальными переносами и alias’ами таблиц:
import java.util.List;
import org.springframework.data.jpa.repository.Query;
@Query(
value = """
select p.*
from product p
join category c on c.id = p.category_id
where c.code = :code
order by p.name asc
""",
nativeQuery = true
)
// Алиасы p/c: не "для понтов", а чтобы запрос читался и не расползался в кашу
// Переносы строк в text block: проще сравнивать с тем, что вы гоняете в SQL-консоли
List<Product> findByCategoryCodeNative(String code);
Второй момент — алиасы таблиц (p, c). Они не «для понтов», они для того, чтобы запрос читался и не превращался в кашу, особенно когда join’ов больше одного. Даже в маленьком запросе product p выглядит приятнее, чем постоянное product. и category..
Третий момент — параметры. В native SQL вы всё так же можете использовать named parameters (если вы уже привыкли к ним на JPQL‑уровне), и это резко снижает шанс перепутать, что такое ?1 и где у вас там порог, а где код категории.
И четвёртый момент — имя метода. Вопрос “как назвать метод” здесь особенно важен, потому что native SQL — это implementation detail, а метод репозитория — часть вашего контрактного API в feature‑пакете. Название должно отвечать на бизнес‑вопрос (например, findByCategoryCode…), а не сообщать миру «смотрите, я умею native». Если назвать метод findAllNativeProductsBecauseImCool(), вы получите не уважение, а лёгкую тоску у будущего читателя. Скорее всего, у самого себя.
Наконец, маленькая, но полезная проверка на здравый смысл: если ваш SQL‑запрос в аннотации выглядит так, будто вы пытаетесь написать дипломную работу по SQL‑синтаксису, остановитесь. Native query в рамках Spring Data JPA проекта должна быть короткой, локальной и понятной. Если она становится монстром — обычно это сигнал, что вы пытаетесь решить слишком много одним запросом, или у вас уже начинается отдельная подсистема отчётности.
8. Типичные ошибки при работе с native query
Ошибка №1: использовать entity-имена в native SQL.
Это самая частая «ошибка переключения режима». В голове ещё JPQL, рука пишет from Product p, а nativeQuery = true уже включён. В итоге PostgreSQL честно отвечает, что таблицы Product у него нет. В SQL вы пишете from product, и только так.
Ошибка №2: использовать Java-имена полей вместо SQL-колонок.
В проекте мы часто используем camelCase в Java и snake_case в БД. Поэтому availableQuantity и available_quantity — разные сущности. В JPQL вы можете писать availableQuantity, потому что вы обращаетесь к полю. В native SQL вы обязаны писать available_quantity, потому что вы обращаетесь к колонке.
Ошибка №3: делать частичный select, но возвращать entity.
Если метод возвращает Product, запрос должен вернуть “форму продукта” — набор колонок, достаточный для маппинга. Попытка выбрать пару колонок и вернуть entity приводит к ошибкам маппинга или к странным “полупустым” объектам. Если нужны 2–3 поля, лучше возвращать не entity, а специальный read‑результат.
Ошибка №4: забывать, что join в SQL — это join по условию.
JPQL позволяет написать join p.category c и не думать о FK. В native SQL условие on c.id = p.category_id — ваша ответственность. Если вы его забыли или написали не по тем колонкам, база не будет «догадываться», что вы имели в виду.
Ошибка №5: писать SQL одной длинной строкой и терять читабельность.
Когда SQL в аннотации превращается в одну строку на 200 символов, любая ошибка становится квестом “найди лишний пробел”. Java text blocks и аккуратные переносы — это не эстетика, это снижение времени на отладку. И да, это один из редких случаев, когда «красиво оформленный текст» реально экономит деньги. Ваши. На вашей же нервной системе.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ