1. Роль Flyway і Hibernate в Boot
До цього моменту схема вже живе в репозиторії: ми зафіксували початковий V1–V3 набір і домовилися, як еволюціонувати його далі без ручних ALTER TABLE. Тепер лишається вбудувати це у звичайний запуск застосунку, щоб база підіймалася однаково на будь-якій машині. Тоді й стає зрозуміло, що означає «Flyway інтегровано» і яку роль після цього відіграє Hibernate.
Давайте чесно: міграція, яку не застосовують автоматично, дуже швидко перетворюється на гарну легенду. Сьогодні ви памʼятаєте, що потрібно виконати V1, V2, V3, а через тиждень забудете, а новий розробник (або ви самі в майбутньому) створить базу «як-небудь» і дивуватиметься, чому падає запит. Інтеграція Flyway у Spring Boot потрібна саме для того, щоб не було окремого ритуалу «підготовки бази». Ви запускаєте застосунок — і він сам робить мінімально необхідне: перевіряє історію міграцій, накочує відсутні версії, а вже потім підіймає JPA-інфраструктуру.
Друга причина — розділення відповідальності. До Flyway ви часто живете в режимі «Hibernate створить або оновить таблиці, а я потім розберуся». Після Flyway схема має жити в SQL-міграціях, а Hibernate має перестати бути джерелом істини. Його нова роль — перевірка відповідності: «ваші entity збігаються з тим, що реально є в базі». Саме тут дуже добре підходить режим ddl-auto: validate. Hibernate нічого не змінює, але чесно свариться, якщо ви забули додати колонку міграцією або назвали її інакше.
Канонічний маршрут для поточного shop-data-jpa
Якщо зібрати все в один робочий сценарій, він виглядає так:
- Спочатку фіксуємо поточну узгоджену схему проєкту в початковому наборі:
V1__init_schema.sql
V2__init_indexes.sql
V3__seed_reference_data.sql
- Кладемо ці файли в src/main/resources/db/migration і додаємо flyway-core, щоб міграції стали частиною звичайного шляху запуску.
- Перемикаємо Hibernate на ddl-auto: validate, щоб схема змінювалася тільки через міграції, а не через ddl-auto=update.
- Для навчального переходу беремо чисту PostgreSQL-базу, а не намагаємося поєднати новий Flyway з уже накопиченою локальною схемою.
- Запускаємо застосунок: спочатку Flyway застосовує відсутні версії, потім Hibernate перевіряє мапінг.
- Перевіряємо, що застосунок стартував і що flyway_schema_history справді містить записи про застосовані міграції.
2. Підключення Flyway у Gradle
Тут хочеться почати дуже натхненно, але правда в тому, що підʼєднання Flyway — один із найменш драматичних кроків у житті. Ми додаємо одну залежність, і Spring Boot (як платформа, яка любить автоматизувати корисні речі) сам створює потрібні біни та запускає міграції під час старту. Важливо лише розуміти, що саме має бути в build.gradle.kts, аби автоконфігурація спрацювала.
У нашому проєкті shop-data-jpa Flyway має підʼєднуватися як звичайна runtime-залежність. Якщо ви використовуєте Gradle, зазвичай достатньо ось такого фрагмента:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa") // JPA-інфраструктура (Hibernate, транзакції тощо)
implementation("org.flywaydb:flyway-core") // Flyway: застосовує міграції на старті застосунку
runtimeOnly("org.postgresql:postgresql") // Драйвер PostgreSQL: потрібен тільки під час запуску
}
Зверніть увагу на логіку цього фрагмента. spring-boot-starter-data-jpa приносить JPA-інфраструктуру, драйвер Postgres потрібен, щоб реально підключитися до бази, а flyway-core — щоб міграції стали частиною запуску. Після додавання flyway-core Spring Boot побачить бібліотеку на classpath і (за наявності DataSource) застосує автоконфігурацію для Flyway. Тобто, грубо кажучи, ви додали залежність — і в застосунку зʼявився свій «мозок» запуску схеми БД.
Іноді студенти в цей момент запитують: «А де ми створюємо обʼєкт Flyway вручну?» Відповідь схожа на відповідь про Spring Data репозиторії: «Ніде, Boot зробить це за вас». Але важливо, щоб ви не сприймали це як магію: Boot просто знає типові правила звʼязування і дотримується їх.
3. Папка міграцій: db/migration
Коли ви вперше бачите Flyway, дуже хочеться покласти міграції кудись «логічно», наприклад у src/main/java поруч із entity або в окремий пакет com.example.shopdatajpa.migrations. Але Flyway мислить простіше: міграції — це ресурси застосунку, тобто файли, які потрапляють у classpath. Тому ми тримаємо їх у resources, у стандартній папці db/migration, щоб Spring Boot і Flyway знайшли їх без додаткових налаштувань.
У нашому проєкті після фіксації початкового набору V1–V3 структура зазвичай виглядає так:
src/main/resources
|-- application.yml
`-- db
`-- migration
|-- V1__init_schema.sql
|-- V2__init_indexes.sql
`-- V3__seed_reference_data.sql
Чому саме db/migration? Бо це типова домовленість Flyway: якщо не сказано інакше, він дивиться в classpath:db/migration. Це корисно з двох причин. По-перше, ви одразу отримуєте «працює з коробки» без зайвого конфігураційного шуму. По-друге, будь-який розробник, який прийшов у проєкт, миттєво розуміє: «ага, міграції ось тут».
Типова помилка новачка — створити папку migrations або sql і потім довго налаштовувати spring.flyway.locations, а ще довше пояснювати команді, чому в нас «не як у всіх». У навчальному проєкті краще триматися максимально передбачуваних домовленостей: це знижує ймовірність поломок і економить розумові ресурси на справді важливі теми.
4. Налаштування spring.flyway і ddl-auto
Коли кажуть «налаштувати Flyway», багато хто очікує велику простиню YAML на пів екрана. На практиці хороше налаштування — це найчастіше мінімальне налаштування. Flyway за замовчуванням уміє дуже багато, і якщо ви не робите нічого нестандартного, краще не ускладнювати. У цьому розділі ми налаштуємо Flyway так, щоб він точно працював у нашому проєкті, і водночас конфіг залишався читабельним.
Мінімальний приклад у application.yml може виглядати так:
spring:
flyway:
enabled: true # Явно фіксуємо, що Flyway увімкнено (навіть якщо автоконфігурація і так його підніме)
jpa:
hibernate:
ddl-auto: validate # Hibernate не змінює схему, а лише перевіряє її відповідність entity
Тут важливі дві речі. Перша: enabled: true зазвичай навіть не обовʼязкове, бо Flyway увімкнеться сам, якщо залежність на місці. Але в навчальному проєкті «явне краще за неявне»: ми спеціально показуємо, що Flyway у нас увімкнено свідомо, а не «ну він якось сам».
Друга: ddl-auto: validate — це вже місток до наступного розділу, але логічно він живе поруч. Ви прямо в конфігу фіксуєте нову дисципліну: міграції створюють або змінюють схему, а Hibernate лише перевіряє відповідність.
Якщо вам хочеться проговорити, де Flyway шукає міграції, можна (не обовʼязково, але іноді корисно для ясності) додати locations:
spring:
flyway:
enabled: true
locations: classpath:db/migration # Явно вказуємо розташування міграцій у classpath
Функціонально це нічого не змінює порівняно з типовим значенням, але допомагає новачкові не гадати, звідки Flyway бере файли. У навчальних проєктах це іноді виправдано як «коментар у конфігу».
Окрема важлива думка: Flyway працює через той самий DataSource, що й JPA/Hibernate. Тобто якщо у вас зламаний URL до Postgres або пароль, Flyway також не запуститься. Це логічно: він має підʼєднатися до бази, щоб створити таблиці та записати історію міграцій.
5. Режим ddl-auto після Flyway
Перехід на Flyway — це не «поставили ще одну бібліотеку». Це зміна головного джерела правди. Якщо залишити ddl-auto=update, ви отримаєте ситуацію, де схема змінюється двома незалежними механізмами, і це дуже схоже на спробу одночасно керувати машиною і велосипедом. У цьому розділі ми розберемо режими ddl-auto і виберемо той, що відповідає міграційно-орієнтованому підходу.
Давайте згадаємо, що взагалі означає ddl-auto (на рівні змісту, без академічної мороки):
| Значення ddl-auto | Що робить Hibernate | Чому це (не) підходить після Flyway |
|---|---|---|
| update | намагається підлаштовувати схему під entity | виглядає зручно, але створює неявні зміни і ламає відтворюваність |
| create | відтворює схему з нуля під час старту | видалить дані, підходить лише для дуже тимчасових демо |
| create-drop | створює під час старту і видаляє під час завершення | зручно для експериментів, але не для нормального життя |
| validate | нічого не змінює, лише перевіряє відповідність | ідеальний «контролер» поруч із Flyway |
| none (або відсутність) | взагалі не торкається схеми | іноді це нормально, але ви втрачаєте автоматичну перевірку мапінгу |
Чому update — погана ідея після Flyway? Бо Flyway будує історію схеми як послідовність версій, і ця історія має бути повною: «ось що ми зробили, ось чому, ось коли». ddl-auto=update змінює базу під час старту застосунку, і зміни ніде не фіксуються в міграціях. У підсумку у вас може бути база, яка «трохи еволюціонувала» від entity-моделі, а міграції про це не знають. На іншій машині цього еволюційного шляху не буде — і почнуться дивні помилки.
Тому дуже практичний режим після впровадження Flyway — це:
spring:
jpa:
hibernate:
ddl-auto: validate # Після Flyway Hibernate лише перевіряє схему, але не править її сам
Тоді у вас зʼявляється корисний захист. Якщо ви додали поле в entity і забули міграцію, Hibernate скаже: «Колонки немає». Якщо ви перейменували колонку в міграції і забули оновити @Column, Hibernate скаже: «Не збігається». Це саме те, чого ми хочемо: помилки схеми ловляться на ранньому старті застосунку, а не посеред робочого дня, коли ви вже «трохи все змінили».
6. Порядок старту: Flyway → Hibernate
Поки ви не побачили це на практиці, усе звучить як теорія: «Flyway застосує, Hibernate перевірить». Насправді порядок старту критичний. Якщо JPA-інфраструктура підніметься раніше, ніж схема буде створена, ви отримаєте помилки на кшталт «relation does not exist». Тому Spring Boot робить розумну річ: мігрує базу до створення EntityManagerFactory. Тут ми зафіксуємо послідовність подій і навчимося читати логи старту.
Умовна схема запуску виглядає так:
flowchart TD
%% Порядок важливий: міграції мають застосуватися до ініціалізації JPA-інфраструктури
A[Spring Boot стартує] --> B[Створюється DataSource]
B --> C[Flyway перевіряє історію міграцій]
C --> D[Flyway застосовує відсутні версії]
D --> E["Hibernate ddl-auto=validate перевіряє схему"]
E --> F[Ініціалізуються репозиторії Spring Data]
F --> G[Застосунок готовий обслуговувати сценарії використання]
Як це проявляється в логах? Приблизно так: ви побачите повідомлення Flyway про версію і про застосування міграцій, потім (якщо увімкнено) повідомлення Hibernate про перевірку схеми і далі стандартний старт Spring Boot. Вам не потрібно запамʼятовувати точні рядки, важливо вловити структуру: «спочатку Flyway, потім Hibernate».
Це — одна з причин, чому інтеграція Flyway у Boot така зручна. Вам не потрібно писати окремий «запускач міграцій» або памʼятати порядок дій вручну. Ви просто запускаєте застосунок, і він або стартує в коректному стані, або падає, але падає чесно й рано, коли ще нічого не встигло «напівзламатися».
7. Перехід із ddl-auto на Flyway
Ось тут зазвичай трапляється перший «бойовий» сюрприз. До сьогодні схема могла створюватися Hibernateʼом автоматично. Отже, у вашій локальній базі вже є таблиці product, category і всі інші. Тепер ви додаєте Flyway з міграціями V1__init_schema.sql, запускаєте застосунок… і Flyway каже щось на кшталт «таблиця вже існує». Це не вада Flyway, а логіка: він вважає, що міграції мають застосовуватися до чистої бази (або хоча б до бази з коректною історією).
У навчальному проєкті найпростіший і найчесніший шлях — підняти чисту базу заново. Якщо ви використовуєте Docker Compose, це часто означає «прибрати volume і стартувати знову». На практиці команда може виглядати так:
docker compose down -v # Зупиняємо контейнери і видаляємо volume з даними (отримуємо «чисту» базу)
docker compose up -d # Підіймаємо контейнери знову у фоні
Після цього база буде порожньою, Flyway спокійно застосує V1, V2, V3, створить таблицю історії міграцій (зазвичай flyway_schema_history), і далі Hibernate в режимі validate підтвердить, що все збіглося.
Так, існує й інший шлях — задати вже наявній базі baseline і сказати Flyway вважати поточний стан початковою точкою. Але для нашого навчального проєкту це не основний маршрут: він потрібен там, де не можна втратити наявну схему й дані. Тут простіше і чесніше один раз підняти чисту БД і переконатися, що V1__init_schema.sql, V2__init_indexes.sql і V3__seed_reference_data.sql справді відновлюють проєкт з нуля.
8. Smoke-перевірка старту
Нам потрібно не лише налаштувати конфіг, а й переконатися, що дисципліна справді працює: база піднімається міграціями, Hibernate перевіряє схему, репозиторії живі, і застосунок стартує без ручних кроків. Тут важливо зробити перевірку максимально простою: ми не будуємо веб-рівень, не пишемо новий функціонал — ми просто перевіряємо, що інфраструктура «дихає».
Один із найзрозуміліших способів — використати CommandLineRunner, який виконається після старту контексту, зробить один запит до репозиторію і виведе результат у лог. Наприклад, у catalog-модулі (або в common-пакеті, якщо у вас уже є місце під такі smoke-перевірки) можна зробити так:
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration // Конфігураційний клас, який потрапить у Spring Context
public class StartupSmokeConfig {
@Bean // Реєструємо CommandLineRunner як bean, щоб він виконався на старті застосунку
CommandLineRunner smoke() {
// Якщо виконання дійшло до цієї точки, значить:
// 1) контекст піднявся;
// 2) Flyway зміг підʼєднатися до бази та застосувати міграції;
// 3) Hibernate в режимі validate не впав на невідповідності схеми.
return args -> System.out.println("Застосунок запущено, база даних готова");
}
}
Цей приклад спеціально максимально простий: він не перевіряє таблиці напряму, але якщо ваш застосунок стартував до виконання runnerʼа, це вже означає, що міграції Flyway пройшли, Hibernate validate не впав, і контекст піднявся. Якщо хочеться зробити перевірку трохи змістовнішою, можна, наприклад, викликати count() у репозиторію. Але тут важливий баланс: ми не перетворюємо лекцію про Flyway на лекцію про тестування або про створення демоданих. Наша мета — побачити, що запуск тепер самодостатній.
Цього вже достатньо для smoke-check: runner виконається тільки після того, як контекст піднявся. А щоб перевірити саме міграційний шлях, подивіться в flyway_schema_history: після чистого старту там мають бути V1__init_schema.sql, V2__init_indexes.sql і V3__seed_reference_data.sql. Тоді ви бачите не лише «застосунок стартував», а й те, що схема піднята саме через історію міграцій.
І ще одна важлива деталь: коли в проєкті є міграції, «успішний старт на чистій базі» стає дуже сильним маркером якості. Ви можете будь-коли видалити базу, підняти її заново і бути впевненими, що схема відновиться. Це відчуття спочатку здається дрібницею, а потім раптом стає різницею між «я контролюю проєкт» і «проєкт контролює мене».
9. Типові помилки під час інтеграції Flyway у Spring Boot
У цьому місці зазвичай зʼясовується, що проблеми виникають не через «складність Flyway», а через те, що ми намагаємося жити одразу в двох світах: міграції як джерело правди і автогенерація схеми як звична тимчасова підпора. Помилки нижче трапляються настільки часто, що їх можна вважати частиною навчальної програми, навіть якщо ніхто цього не планував. Хороша новина: майже всі вони лікуються дисципліною і парою зрозумілих правил.
Помилка № 1: підʼєднали Flyway, але залишили ddl-auto=update.
Це виглядає невинно, бо «ну нехай Hibernate трохи допомагає». На практиці ви отримуєте схему, яка змінюється то міграціями, то автооновленням під час старту, і дуже швидко перестаєте розуміти, звідки взялася конкретна колонка. Правильна позиція після ввімкнення Flyway — поставити ddl-auto: validate (або щонайменше none), щоб Hibernate перестав бути автором схеми.
Помилка № 2: міграції лежать не в db/migration, і ви забули налаштувати locations.
Симптом простий: застосунок стартує, але таблиці не створюються, а Flyway ніби «нічого не робить». Потім виявляється, що файли лежать у resources/migrations або в resources/sql. Якщо ви хочете типову поведінку — використовуйте типову папку. Якщо ж хочете кастомну — налаштуйте spring.flyway.locations, але тоді будьте готові пояснювати це кожному новачкові в проєкті (включно з вами через місяць).
Помилка № 3: запускаєте Flyway на базі, де таблиці вже створені вручну або через ddl-auto.
Це класична ситуація під час переходу на міграції. Flyway намагається застосувати V1, а таблиця вже існує. У навчальному проєкті найпростіший вихід — підняти чисту базу (видалити docker volume) і запустити міграції з нуля. Це простіше, чесніше і формує правильну звичку: міграції мають уміти підняти схему на порожній базі.
Помилка № 4: міграції застосовуються, але Hibernate в validate падає — і ви не розумієте чому.
Зазвичай причина в дрібницях: імʼя колонки відрізняється на один символ, в entity стоїть @Column(name="created_at"), а в SQL-міграції createdAt, або тип numeric(12,2) у базі не збігається з очікуваннями мапінгу. Тут важливо прийняти: validate — не ворог, а ваш «лінтер схеми». Він не ламає базу, він показує розсинхронізацію, яку інакше ви знайшли б набагато пізніше.
Помилка № 5: очікування, що Flyway «побачить» ручні правки і якось їх врахує.
Flyway не телепат. Якщо ви додали колонку вручну через ALTER TABLE у консолі, Flyway про це не дізнається, а історія міграцій не поповниться. У результаті «на вашій машині працює», а на чистій базі — ні. Рішення одне: будь-яка зміна схеми — тільки через новий migration-файл, який комітиться разом із кодом.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ