JavaRush /Курси /Hibernate deep-dive /Перший запуск у діагностичному режимі

Перший запуск у діагностичному режимі

Hibernate deep-dive
Рівень 1 , Лекція 4
Відкрита

1. Мікроскоп для курсу 🔬

Підготовчі дні на технічних курсах мають погану репутацію. І часто цілком заслужено: пів дня йде на встановлення, налаштування та боротьбу із середовищем, а навчальної цінності майже немає. У Hibernate deep-dive ми спеціально намагаємося цього уникнути. Тому сьогоднішня мета — не просто «запустити проєкт», а підняти діагностичний стенд, на якому будь-який майбутній ORM-сценарій можна буде побачити одразу в кількох площинах: як код, як міграції, як SQL, як bind values і як статистику Hibernate. Це вже зовсім інший рівень користі.

Чому це так важливо? Тому що без спостережуваності deep-dive дуже швидко скочується у ворожіння. Ви бачите дивну поведінку, припускаєте причину, змінюєте анотацію, знову сподіваєтеся на краще — і далі живете в режимі шаманства. Нормальний інженерний курс має вчити протилежного: спершу побудувати середовище, у якому поведінку видно, і лише потім робити висновки.

Ось тут і з’являється перший справді сильний момент цього курсу. Ми не завершуємо рівень фразою «ну, у вас тепер є проєкт». Ми завершуємо його стендом, у якому один і той самий виклик видно і як бізнес-операцію, і як SQL, і як параметри, і як лічильники ORM. Це дуже практична точка старту.

2. PostgreSQL через Docker Compose 🐘

Локальну базу в нашому курсі піднімаємо через Docker Compose. Це не декоративне ускладнення і не спроба «зробити все по-дорослому». Це найпростіший спосіб зробити середовище відтворюваним. Якщо один студент працює з локальним PostgreSQL у системі, другий — із контейнером, третій — з H2 «поки для зручності», а четвертий узагалі не впевнений, до якої бази підключився, то далі ми будемо обговорювати не Hibernate, а чужі середовища.

Мінімальний docker-compose.yml для курсу може виглядати так:

services:
  postgres: # основний сервіс із PostgreSQL
    image: postgres:18 # фіксуємо версію, щоб середовище було відтворюваним
    environment:
      POSTGRES_DB: commerce # імʼя БД, яка створиться під час старту контейнера
      POSTGRES_USER: commerce # користувач БД
      POSTGRES_PASSWORD: commerce # пароль користувача (для курсу — просте поєднання)
    ports:
      - "5432:5432" # виведення порту: зовні localhost:5432 -> всередину контейнера :5432
    volumes:
      - commerce_pgdata:/var/lib/postgresql/data # зберігаємо дані поза контейнером, щоб вони не зникали

volumes:
  commerce_pgdata: # іменований volume для даних PostgreSQL

Тут майже все гранично зрозуміло. Контейнер створює базу commerce, користувача commerce і пароль commerce. Порт виводиться назовні на стандартний 5432, щоб Spring Boot-застосунок міг підключатися до localhost:5432. Том потрібен, щоб дані не зникали під час кожного повторного створення контейнера. Це зручно в звичайній локальній роботі й добре поєднується з ідеєю навчального стенда.

Запуск — звичайний:

docker compose up -d # підняти сервіси у фоновому режимі (detached)
docker compose ps    # перевірити, що контейнер справді запущений і має статус Up

Якщо хочеться переконатися, що контейнер справді живий, а не просто існує уявно, можна подивитися логи:

docker compose logs -f postgres # дивитися логи конкретного сервісу postgres у режимі follow

Хороше локальне базове середовище має бути нудним. У ньому немає неочікуваних портів, немає окремого ручного налаштування, немає окремої інструкції на кожен ноутбук. І це саме те, що нам потрібно. Чим стабільніше й передбачуваніше середовище, тим цікавішою стає поведінка ORM.

3. Базова конфігурація й профілі: local, sql-trace, stats

Наступний крок — розділити режими запуску так, щоб середовище не перетворювалося на один гігантський YAML-файл із вічним шумом. У проєкті є три основні профілі: local, sql-trace і stats. local відповідає за звичайний локальний запуск і підключення до PostgreSQL. sql-trace — за видимість SQL і bind values. stats — за вмикання Hibernate statistics. Це дуже просте розділення, але воно одразу робить стенд керованим. 📌

Базові правила курсу зручно тримати в application.yml:

spring:
  application:
    name: commerce-persistence-lab # імʼя застосунку (для логів, метрик та ідентифікації)

  jpa:
    open-in-view: false # не тримаємо persistence context відкритим до шару view
    hibernate:
      ddl-auto: none # схему НЕ генерує Hibernate, лише Flyway

  flyway:
    enabled: true # міграції ввімкнено як базовий механізм керування схемою

Тут важливі три рядки. open-in-view: false одразу фіксує, що робота з БД має бути всередині зрозумілих меж бекенда, а не витікати далі. ddl-auto: none означає, що Hibernate не керує схемою сам. Схема живе лише через Flyway. І flyway.enabled: true підкреслює, що міграції — не додаткова опція, а частина базового режиму проєкту.

Профіль local зазвичай зберігає підключення до бази:

spring:
  config:
    activate:
      on-profile: local # цей блок застосовується лише за активного профілю local

  datasource:
    url: jdbc:postgresql://localhost:5432/commerce # підключаємося до PostgreSQL із Docker Compose
    username: commerce # має збігатися з POSTGRES_USER
    password: commerce # має збігатися з POSTGRES_PASSWORD

Профіль sql-trace вмикає видимість SQL і параметрів:

spring:
  config:
    activate:
      on-profile: sql-trace # вмикаємо цей блок лише тоді, коли хочемо бачити SQL і параметри

logging:
  level:
    org.hibernate.SQL: debug # друкуємо SQL, який генерує Hibernate
    org.hibernate.orm.jdbc.bind: trace # друкуємо bind values (що саме підставилося в '?')

А профіль stats вмикає збирання статистики:

spring:
  config:
    activate:
      on-profile: stats # вмикаємо статистику лише за потреби, щоб не шуміти постійно

  jpa:
    properties:
      hibernate.generate_statistics: true # Hibernate починає збирати Statistics (лічильники)

Такий розподіл дуже корисний методично. Ви можете запускати застосунок просто в local, коли вам потрібен тихий режим. І можете додавати sql-trace,stats, коли починаєте розбирати поведінку. Тобто шум вмикається усвідомлено, а не висить фоном постійно. Це доросла звичка, і вона чудово переноситься в реальну розробку.

4. Flyway: схема має бути видимою

У deep-dive курсі з Hibernate схема бази — це не технічна тінь від Java-класів. Це повноцінна частина системи. Якщо ви не контролюєте, які таблиці, обмеження й індекси реально існують у PostgreSQL, то згенерований SQL стає важко інтерпретувати. Саме тому проєкт принципово живе на Flyway, а не на ddl-auto=update.

Нехай стартова міграція виглядає дуже просто:

-- V1__baseline_catalog.sql
create table product (
    id bigint primary key, -- явний PK (важливо для ORM і для читабельності моделі)
    sku varchar(64) not null unique, -- бізнес-ідентифікатор, робимо NOT NULL + UNIQUE
    name varchar(255) not null -- імʼя продукту, теж NOT NULL для простого baseline
);

-- стартові дані: щоб із першого запуску було що читати з бази
insert into product(id, sku, name)
values (1, 'SKU-0001', 'Coffee mug');

Цього вже достатньо, щоб побачити, як застосунок застосовує схему й отримує стартові дані. Після старту ви можете перевірити, що Flyway справді спрацював, не лише за логами Spring Boot-застосунку, а й напряму в базі. Це хороший анти-магічний жест. Наприклад:

docker compose exec postgres psql -U commerce -d commerce \ # зайти в контейнер і виконати команду psql
  -c "select installed_rank, version, description, success from flyway_schema_history order by installed_rank;" # перевірити історію міграцій

Якщо таблиця flyway_schema_history існує й показує успішно застосовані міграції, значить схема живе саме так, як задумано. А щоб перевірити наявність стартових даних, можна зробити ще один чесний запит:

docker compose exec postgres psql -U commerce -d commerce \
  -c "select count(*) from product;" # перевіряємо, що стартові дані справді вставилися

Цей момент здається дрібним налаштуванням, але насправді він будує довіру до курсу. Ви не просто сподіваєтеся, що схема піднялася. Ви бачите, як і чим вона піднялася. А отже, далі згенерований SQL можна читати на міцному фундаменті, а не на здогадках.

5. Hibernate statistics і smoke-сервіс

Тепер потрібно додати два невеликі технічні шматки, які дадуть нам реальну спостережуваність. Перший — доступ до Statistics через поточний SessionFactory. Другий — простий smoke-сервіс, який виконує пару операцій читання й друкує коротке зведення. Це як перший прогін лабораторії: ми не робимо нічого складного, але одразу бачимо, що діагностика справді працює.

Конфігурація для Statistics може виглядати так:

@Configuration
class HibernateStatisticsConfig {

    @Bean
    Statistics hibernateStatistics(EntityManagerFactory emf) {
        // unwrap потрібен, щоб дістатися до «нативного» SessionFactory Hibernate з JPA-обгортки
        return emf.unwrap(SessionFactory.class).getStatistics(); // беремо єдиний обʼєкт Statistics
    }
}

Далі маленький сервіс для очищення й друку лічильників:

@Service
class HibernateStatsPrinter {

    private final Statistics stats; // сюди інжектиться Statistics, якщо активний профіль stats

    HibernateStatsPrinter(Statistics stats) {
        this.stats = stats;
    }

    public void clear() {
        stats.clear(); // скидаємо лічильники, щоб рахувати лише поточний сценарій
    }

    public void print() {
        // Prepared statements: скільки JDBC statements підготував Hibernate
        System.out.println("Кількість підготовлених SQL statements = " + stats.getPrepareStatementCount());
        // Entity loads: скільки entity реально завантажилося (корисно для пошуку N+1 і зайвих читань)
        System.out.println("Кількість завантажених сутностей = " + stats.getEntityLoadCount());
        // Flushes: скільки разів відбувався flush (важливо розуміти, коли Hibernate пише в БД)
        System.out.println("Flushes = " + stats.getFlushCount());
    }
}

А тепер — сам smoke-сценарій. Важливо, щоб це була нормальна backend-операція, а не випадковий static helper. Тому нехай він живе як звичайний сервіс:

@Service
class StartupSmokeService {

    private final ProductRepository products; // Spring Data репозиторій для доступу до product
    private final HibernateStatsPrinter statsPrinter; // друк статистики після сценарію

    StartupSmokeService(ProductRepository products, HibernateStatsPrinter statsPrinter) {
        this.products = products;
        this.statsPrinter = statsPrinter;
    }

    @Transactional(readOnly = true) // важливий момент: читаємо в транзакції (базовий режим для JPA)
    public void run() {
        statsPrinter.clear(); // починаємо з чистого аркуша

        long count = products.count(); // швидкий запит, щоб побачити SQL і підготовку statement
        System.out.println("Кількість продуктів у БД = " + count);

        // вибірка за PK: тут якраз зʼявиться bind parameter (id = ?)
        products.findById(1L).ifPresent(product ->
                System.out.println("Завантажений SKU продукту = " + product.getSku())
        );

        statsPrinter.print(); // виводимо лічильники Hibernate саме по цьому сценарію
    }
}

Щоб усе це справді виконалося на старті, зручно підвісити CommandLineRunner:

@Configuration
class SmokeStartupConfig {

    @Bean
    CommandLineRunner smokeRunner(StartupSmokeService smokeService) {
        // CommandLineRunner запускається одразу після старту контексту Spring Boot
        return args -> smokeService.run(); // викликаємо наш smoke-сценарій
    }
}

Тут майже немає бізнес-логіки, і це нормально. Мета не в тому, щоб «зробити фічу». Мета в тому, щоб довести: проєкт реально стартує, реально застосовує міграції, реально звертається до PostgreSQL, реально пише SQL у логи й реально віддає статистику по цій операції. Це і є перша робоча перемога сьогоднішнього дня.

6. Що ви маєте побачити в логах і консолі 👀

Тепер запускаємо застосунок у повному діагностичному режимі:

./gradlew bootRun --args="--spring.profiles.active=local,sql-trace,stats" # активуємо профілі: база + SQL/bind + statistics

Якщо ви запускаєте його з IDE, сенс залишається тим самим: мають бути активні профілі local, sql-trace, stats. Важливо не стільки, звідки ви запускаєте, скільки щоб запуск був відтворюваним і відповідав базовому рівню курсу.

Після старту ви маєте побачити приблизно таку послідовність подій. Спершу піднімається DataSource. Потім Flyway повідомляє про застосовані міграції. Далі спрацьовує smoke-сервіс. І ось тут з’являються головні сигнали:

# Нижче — приклад ключових рядків (вони можуть відрізнятися форматуванням, але зміст той самий)
DEBUG org.hibernate.SQL - select count(p1_0.id) from product p1_0
DEBUG org.hibernate.SQL - select p1_0.id,p1_0.name,p1_0.sku from product p1_0 where p1_0.id=?
TRACE org.hibernate.orm.jdbc.bind - binding parameter (1:BIGINT) <- [1]
Кількість продуктів у БД = 1
Завантажений sku продукту = SKU-0001
Кількість підготовлених SQL statements = 2
Кількість завантажених сутностей = 1
Flushes = 0

Що тут важливо побачити на власні очі? По-перше, SQL справді видно. По-друге, bind value справді видно. Тобто ? у запиті не лишається абстракцією — ви бачите конкретний параметр 1. По-третє, statistics справді рахують роботу Hibernate, а не існують десь у теорії. По-четверте, smoke-сервіс підтверджує, що операція справді дійшла до бази й прочитала дані.

Ось він, справді сильний момент сьогоднішнього дня: один крихітний use case уже видно одразу в кількох площинах. У вас є Java-код. У вас є міграція, яка створила таблицю й стартові дані. У вас є SQL. У вас є bind parameters. У вас є лічильники Hibernate. Це саме той тип практичного результату, який потрібен deep-dive курсу. Не «застосунок якось запустився», а «у мене є робочий діагностичний стенд». 🔬

7. Чек-лист робочого діагностичного стенда

Щоб цей результат можна було переносити далі по курсу, корисно зафіксувати короткий чек-лист. Якщо колись по дорозі ви перестанете бачити один із цих сигналів, це вже привід не аналізувати Hibernate далі, а спершу відновити сам стенд.

Що перевірити Ознака, що все гаразд
PostgreSQL піднято docker compose ps показує запущений контейнер postgres
Застосунок підключився до правильної бази DataSource вказує на jdbc:postgresql://localhost:5432/commerce
Схему створено через Flyway є таблиця flyway_schema_history, міграції позначено як успішні
ORM не змінює схему сам ddl-auto=none
Контекст не розтягнуто штучно open-in-view=false
SQL видно активний профіль sql-trace, логи org.hibernate.SQL з’являються
Параметри видно активний логер org.hibernate.orm.jdbc.bind
Statistics рахує профіль stats активний, лічильники виводяться
Smoke-сценарій справді сходив у БД є count() / findById() і зрозумілий вивід у консолі

Це і є перший відчутний результат дня. Його можна використовувати не лише сьогодні. Він стане в пригоді щоразу, коли в курсі знадобиться зрозуміти, чи справді ви спостерігаєте поведінку ORM, а не просто дивитеся на красивий Java-код без зворотного зв’язку від бази.

8. Типові помилки й міст до наступного кроку 🚧

Помилка №1: зупинитися на факті старту застосунку.
Boot-застосунок може стартувати, а корисного сигналу від шару даних ви при цьому так і не отримаєте. Без smoke-сценарію й логів ви все ще працюєте майже наосліп.

Помилка №2: увімкнути лише SQL, але не bind values.
Текст запиту сам по собі корисний, але дуже часто половина сенсу сидить саме в реальних параметрах. Без них діагностика швидко стає неповною.

Помилка №3: залишити ddl-auto=update заради зручності.
Це миттєво підриває довіру до схеми. А курс, який вчить пов’язувати Java і SQL, не може дозволити собі плаваючу структуру БД.

Помилка №4: тримати всі логи ввімкненими постійно й перестати їх читати.
Профілі потрібні саме для того, щоб спостережуваність вмикалася усвідомлено й не перетворювалася на білий шум.

1
Опитування
Глибинний Hibernate, рівень 1, лекція 4
Недоступний
Глибинний Hibernate
Розуміння ORM і SQL
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ