JavaRush /Курсы /Spring Data JPA /Native query: от entity к таблицам

Native query: от entity к таблицам

Spring Data JPA
13 уровень , 1 лекция
Открыта

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 и аккуратные переносы — это не эстетика, это снижение времени на отладку. И да, это один из редких случаев, когда «красиво оформленный текст» реально экономит деньги. Ваши. На вашей же нервной системе.

1
Задача
Spring Data JPA, 13 уровень, 1 лекция
Недоступна
Один смысл двумя языками — JPQL и native SQL
Один смысл двумя языками — JPQL и native SQL
1
Задача
Spring Data JPA, 13 уровень, 1 лекция
Недоступна
Поиск складской записи по SKU через SQL join
Поиск складской записи по SKU через SQL join
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ