JavaRush /Курси /Spring Data JPA /JPQL за entity-моделлю

JPQL за entity-моделлю

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

1. JPQL: сутності замість таблиць

Якщо SQL — це мова спілкування з базою даних безпосередньо («таблиці, стовпці, join по FK»), то JPQL — це мова спілкування з вашою об’єктною моделлю («сутності, поля, зв’язки»). За формою він схожий на SQL, щоб наш мозок не злякався одразу, але за змістом ближчий до Java: ви працюєте з іменами класів та їхніми полями. Можна сказати, JPQL — це SQL, який пройшов курси «як говорити на camelCase і не соромитися».

Найважливіше правило тут звучить нудно, зате рятує години життя: у JPQL ми пишемо імʼя сутностіProduct і CustomerOrder, а не product і customer_order. За замовчуванням воно збігається з іменем класу, якщо ви не перевизначали його через @Entity(name = ...). І ми пишемо orderNumber, а не order_number. Чому так? Бо JPQL не знає, як у вас названа таблиця, доки Hibernate не подивиться на мапінг. JPQL знає лише те, що ви описали як сутності та зв’язки.

Давайте зафіксуємо різницю на одному маленькому прикладі. Часто таблиці називаються в snake_case, а поля в Java — у camelCase. І це нормально: база любить підкреслення, Java любить горбики.

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;

@Entity // Це JPA-сутність: у JPQL звертатимемося до сутності за її іменем CustomerOrder
@Table(name = "customer_order") // Це відображення на таблицю в БД, але в JPQL це імʼя не використовується
public class CustomerOrder {

    @Column(name = "order_number", nullable = false, unique = true) // Стовпець order_number, але поле в JPQL — orderNumber
    private String orderNumber;
}

Якщо ви тепер пишете запит, то в JPQL звертаєтеся до поля orderNumber, а не до стовпця order_number. Hibernate вже потім сам «перекладе» це в SQL.

Щоб мозку було простіше перемикатися, тримайте маленьку шпаргалку-таблицю — не для заучування, а просто щоб бачити логіку:

Що ви пишете SQL (про таблиці) JPQL (про сутності)
«Звідки читаємо» from product p from Product p
«За чим фільтруємо» where o.order_number = ... where o.orderNumber = ...
«Як з’єднуємо» join category c on p.category_id = c.id join p.category c
«Куди звертаємося за FK» p.category_id p.category.id або
p.category.code

Так, JPQL виглядає як SQL, але якщо ви почнете писати туди назви таблиць і стовпців, Hibernate відповість помилкою приблизно в стилі: «Я не знаю сутність product». І він матиме рацію: сутність — це Product, а product — це таблиця.

Нижче я знову показуватиму короткі локальні фрагменти репозиторіїв: на них простіше впіймати саму мову JPQL і не загубитися у повному файлі інтерфейсу.

2. Коренева сутність у from

Коли ми читаємо дані, зазвичай інтуїтивно розуміємо, що саме хочемо отримати: список товарів, конкретне замовлення, позиції замовлення або категорії. У JPQL цю інтуїцію треба формалізувати одним рішенням: яка сутність буде кореневою, тобто стоятиме у from. Коренева сутність — це як головний герой у фільмі: можна показувати й інших персонажів, але камера все одно стартує саме з нього.

Наприклад, якщо ви хочете отримати товари, кореневою сутністю стає Product. Якщо ви хочете отримати позиції замовлення — кореневою буде OrderItem. І це рішення впливає на все: на select, на join і навіть на те, як виглядатиме метод репозиторію.

Найпростіший «скелет» JPQL-запиту виглядає так:

select <що повернути>            -- Зазвичай це псевдонім сутності: p, o, oi тощо
from <коренева сутність> <alias> -- Коренева сутність (імʼя сутності), не таблиця
where <умова>                   -- Умови за полями сутностей і шляхами за зв’язками
order by <сортування>           -- Сортування за полями сутностей

У нашому проєкті shop-data-jpa для «читати список товарів» логічно почати з Product:

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

import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {

    @Query("select p from Product p order by p.id") // Product — імʼя сутності; p — псевдонім; сортуємо за полем id
    List<Product> findAllProductsJpql();
}

Зверніть увагу на дві деталі. По-перше, у запиті фігурує Product (імʼя сутності), а не таблиця product. По-друге, p — це псевдонім (alias), він нам потрібен далі, щоб не писати «Product.» щоразу.

І тут виникає хороше практичне запитання: «А чи можна у from поставити будь-яку сутність, а потім дістатися до потрібної?» Технічно ви можете багато чого, але з погляду мислення — краще ні. Коли ви читаєте товари, нехай коренем буде Product. Коли читаєте позиції — нехай буде OrderItem. Це робить запит простішим і читабельнішим: ви одразу розумієте, що повертає метод репозиторію.

3. Псевдоніми (alias)

Псевдонім у JPQL — це, по суті, змінна. Як у Java: ви не пишете щоразу new BigDecimal("10.00"), а зберігаєте значення у змінній і далі працюєте з її іменем. У запиті псевдонім — це коротке імʼя для сутності, щоб звертатися до її полів і зв’язків. Це не «магічна літера», а звичайний інструмент читабельності. І так, у JPQL без нього ви дуже швидко почнете страждати.

Найчастіше псевдоніми беруть короткі, але змістовні: p для Product, c для Category, o для CustomerOrder, oi для OrderItem. Не тому, що так написано в заповідях ORM, а тому, що це зручно й нагадує розмову команди. Коли в запиті три сутності, псевдоніми стають особливо важливими: інакше ви не зрозумієте, до чиїх полів звертаєтеся.

Ось маленька табличка-орієнтир для нашого проєкту — це не правило, а звичка, яка економить нерви:

Сутність Типовий alias
Product p
Category c
CustomerOrder o
OrderItem oi

Тепер подивімося, як alias робить запит читабельним. Без alias вам буде важко нормально написати order by, where і переходи за зв’язками.

import com.example.shopdatajpa.ordering.entity.CustomerOrder;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface CustomerOrderRepository extends JpaRepository<CustomerOrder, Long> {

    @Query("select o from CustomerOrder o where o.totalAmount > 0 order by o.id") // o — псевдонім замовлення; фільтруємо за полем totalAmount
    List<CustomerOrder> findNonZeroOrders();
}

Тут o — це просто коротке імʼя. Жодної містики. Але якщо ви забудете alias і спробуєте написати where totalAmount > 0, JPQL вас не зрозуміє: йому потрібно знати, у якого «об’єкта» це поле.

4. Поля сутності в JPQL

Найчастіша помилка новачка в JPQL — думати, що «це майже SQL, отже стовпці теж майже стовпці». Ні. У JPQL ви звертаєтеся до полів сутності, тобто до тих імен, які бачите в Java-класі. Навіть якщо в базі стовпець називається інакше, JPQL залишається на боці Java-світу.

Наприклад, якщо в Product поле називається sku, то в JPQL воно так і називається: p.sku. Якщо в базі раптом стовпець називається product_sku, а ви зробили @Column(name = "product_sku"), то все одно в JPQLp.sku. JPQL ніби каже: «Я працюю з обʼєктами. Як це називає обʼєкт? Отак і називай».

Для мислення це дуже корисно: ви читаєте запит і одразу зіставляєте його з кодом сутності. Не потрібно стрибати очима між SQL-DDL і Java-класом, щоб зрозуміти, що саме ви фільтруєте.

На такому тлі дуже корисно сприймати вираз p.name як «шлях до поля», майже так, ніби ви писали б у Java p.getName() — тільки без виклику методу, бо це ж запит.

До речі, те саме стосується і @Embeddable (ми вже використовували його на DeliveryAddress). Якщо в замовлення є поле deliveryAddress, а в нього є поле city, то шлях виглядатиме як o.deliveryAddress.city. Це дуже «обʼєктно» і дуже свідомо: ми читаємо за моделлю, а не за таблицями.

5. Навігація за зв’язками

Коли у вас зʼявляються звʼязки (ManyToOne, OneToMany), JPQL стає особливо приємним: ви починаєте писати умови так, ніби справді ходите по обʼєктах. Тобто ви не думаєте «де стовпець FK?», а думаєте «у товару є категорія, у категорії є код».

У проєкті shop-data-jpa у Product є посилання category. Отже, в JPQL ви можете написати:

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

import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {

    @Query("select p from Product p where p.category.active = true") // Переходимо за зв’язком category і фільтруємо за полем active
    List<Product> findProductsFromActiveCategories();
}

Ось що тут важливо відчути: p.category.active виглядає як звичайна навігація по об’єкту. Але під капотом Hibernate зрозуміє, що йому потрібні дані з category, і зробить SQL-join. Тобто ви формулюєте думку за обʼєктною моделлю, а ORM перекладає її в реляційну реальність.

Щоб це не було абстракцією, ось схема зв’язків нашого домену — спрощена, бо мозку іноді потрібен рисунок, а не ще один рядок тексту:

classDiagram
    class Category {
      +Long id
      +String code
      +boolean active
    }

    class Product {
      +Long id
      +String sku
      +String name
      +Category category
    }

    class CustomerOrder {
      +Long id
      +String orderNumber
      +String customerEmail
      +BigDecimal totalAmount
    }

    class OrderItem {
      +Long id
      +CustomerOrder order
      +Product product
      +int quantity
    }

    Category "1" <-- "*" Product : категорія
    CustomerOrder "1" <-- "*" OrderItem : замовлення
    Product "1" <-- "*" OrderItem : товар

Тепер приклад із переходом через OrderItem -> order у запиті. Нам потрібно взяти позиції замовлень конкретного клієнта — поки що залишимо email просто рядком, виключно як демонстрацію форми запиту:

import com.example.shopdatajpa.ordering.entity.OrderItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface OrderItemRepository extends JpaRepository<OrderItem, Long> {

    @Query("""
           select oi
           from OrderItem oi
           join oi.order o
           where o.customerEmail = 'customer@example.com'
           order by oi.id
           """) // Text block зручний для багаторядкового JPQL; join іде за зв’язком oi.order; email захардкожений лише для демо
    List<OrderItem> findItemsOfCustomerDemo();
}

Тут видно два важливі моменти.

Перший: join oi.order o — ми зʼєднуємо не таблицю з таблицею, а зв’язок між сутностями. JPQL не вимагає писати on oi.order_id = o.id, бо це вже «вшито» в мапінг @ManyToOne.

Другий: у where ми використовуємо поле customerEmail у сутності CustomerOrder, а не стовпець customer_email. Тобто знову — обʼєктна модель.

Можна запитати: «А чому іноді пишуть p.category.code без join, а іноді роблять join oi.order o?» Бо JPQL дозволяє прямо переходити за зв’язками у виразах, і провайдер сам створить потрібні join. Але коли запит стає трохи складнішим або коли ви хочете, щоб той, хто читає код, явно бачив, які сутності беруть участь, явний join робить запит зрозумілішим. Це не релігія, а просто питання читабельності.

6. Як JPQL перетворюється на SQL

JPQL — це не «альтернативна база даних». Сам по собі він нічого до PostgreSQL не надсилає. Він проходить через Hibernate, який розбирає запит, розуміє вашу entity-модель і генерує звичайний SQL. І лише потім SQL їде до PostgreSQL. Якщо тримати це в голові, зникає відчуття: «Я написав якийсь рядок, і сталася магія».

Ось схема цього шляху, буквально як конвеєр:

flowchart TD
    A[Метод репозиторію] --> B[Рядок JPQL]
    B --> C["Hibernate / провайдер JPA"]
    C --> D[SQL для PostgreSQL]
    D --> E[(PostgreSQL)]
    E --> D
    D --> F[Набір результатів]
    F --> G[Сутності через мапінг]

Давайте візьмемо простий JPQL і уявімо, у що він перетворюється. Наприклад:

select p -- У JPQL часто обирають псевдонім сутності, тобто повертають сутність цілком
from Product p
where p.category.code = 'phones' -- Навігація за зв’язком: Product -> Category -> code

Псевдо-SQL приблизно може виглядати так:

select p.* -- У реальному SQL це вже рядки таблиці product (або конкретні стовпці)
from product p
join category c on p.category_id = c.id -- Те, що в JPQL було p.category..., тут виражається через JOIN за FK
where c.code = 'phones'

Чому «псевдо»? Бо точна форма залежить від діалекту, налаштувань, назв таблиць і стовпців, а також від того, як саме Hibernate вирішить побудувати SQL. Але сенс один: p.category.code не існує як «стовпець» у product, тому для фільтра за кодом категорії потрібен join.

Отже, корисна звичка така: коли ви пишете JPQL, ви одночасно тримаєте в голові дві картини. Перша — об’єктна: «Product -> Category -> code». Друга — реляційна: «product join category by FK, filter by category.code». І саме цей міст дає вам доросле ORM-мислення без містики.

7. Типові помилки в JPQL-мисленні

Помилки в JPQL майже завжди не синтаксичні, а світоглядні. Тобто ви пишете запит у стилі SQL, а JPQL чекає стилю роботи із сутностями. І якщо впіймати цю думку зараз, далі буде набагато простіше: ви перестанете боротися з інструментом і почнете ним користуватися.

Помилка №1: писати в JPQL імена таблиць і стовпців.
Дуже типовий старт: рука сама тягнеться написати from product або where order_number = .... Але JPQL живе не у світі DDL, а у світі Java-класів. Тому «правда» для JPQL — це Product, CustomerOrder і orderNumber. Якщо вам хочеться писати snake_case, найімовірніше, ви подумки все ще в SQL, а не в entity-моделі.

Помилка №2: намагатися звертатися до зовнішнього ключа як до category_id.
У таблиці product у вас справді є category_id. Але в сутності Product у вас є посилання category. І JPQL чекає, що ви підете за зв’язком: p.category.id або p.category.code. Коли ви пишете p.category_id, ви ніби намагаєтеся в Java звернутися до поля, якого немає. Hibernate вам приблизно так і відповість: «не знаю такої властивості».

Помилка №3: вибрати неправильну кореневу сутність, а потім дивуватися складності запиту.
Якщо ви хочете отримати список OrderItem, але ставите у from CustomerOrder і далі намагаєтеся «дістатися» до позицій, запит стане довшим і менш читабельним. Корінь запиту — це, по суті, те, що ви повертаєте тому, хто викликає метод. Якщо ви повертаєте товари — починайте з Product. Якщо повертаєте позиції — починайте з OrderItem.

Помилка №4: забувати про alias або плутати alias між сутностями.
JPQL швидко стає нечитабельним, якщо ви неуважні з псевдонімами. Коли p раптом означає і Product, і Price (жарт, але в кожному жарті…), а o раптово стає то замовленням, то «order by» у голові, ви починаєте помилятися на рівному місці. Псевдоніми — це дисципліна, а не «якась літера».

Помилка №5: сприймати join як «зʼєднання таблиць», а не як «перехід за зв’язком».
У SQL ви зʼєднуєте таблиці, бо інакше просто не можете. У JPQL ви зʼєднуєте сутності за зв’язком, який уже описали анотаціями. Тому коректний join виглядає як join oi.order o, а не як спроба вручну зімітувати on. Коли ви думаєте таблицями, ви починаєте сперечатися з мапінгом. Коли думаєте зв’язками, запити стають коротшими.

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