JavaRush /Курси /Spring Data JPA /Інтеграція Flyway у Spring Boot

Інтеграція Flyway у Spring Boot

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

1. Роль Flyway і Hibernate в Boot

До цього моменту схема вже живе в репозиторії: ми зафіксували початковий V1V3 набір і домовилися, як еволюціонувати його далі без ручних ALTER TABLE. Тепер лишається вбудувати це у звичайний запуск застосунку, щоб база підіймалася однаково на будь-якій машині. Тоді й стає зрозуміло, що означає «Flyway інтегровано» і яку роль після цього відіграє Hibernate.

Давайте чесно: міграція, яку не застосовують автоматично, дуже швидко перетворюється на гарну легенду. Сьогодні ви памʼятаєте, що потрібно виконати V1, V2, V3, а через тиждень забудете, а новий розробник (або ви самі в майбутньому) створить базу «як-небудь» і дивуватиметься, чому падає запит. Інтеграція Flyway у Spring Boot потрібна саме для того, щоб не було окремого ритуалу «підготовки бази». Ви запускаєте застосунок — і він сам робить мінімально необхідне: перевіряє історію міграцій, накочує відсутні версії, а вже потім підіймає JPA-інфраструктуру.

Друга причина — розділення відповідальності. До Flyway ви часто живете в режимі «Hibernate створить або оновить таблиці, а я потім розберуся». Після Flyway схема має жити в SQL-міграціях, а Hibernate має перестати бути джерелом істини. Його нова роль — перевірка відповідності: «ваші entity збігаються з тим, що реально є в базі». Саме тут дуже добре підходить режим ddl-auto: validate. Hibernate нічого не змінює, але чесно свариться, якщо ви забули додати колонку міграцією або назвали її інакше.

Канонічний маршрут для поточного shop-data-jpa

Якщо зібрати все в один робочий сценарій, він виглядає так:

  1. Спочатку фіксуємо поточну узгоджену схему проєкту в початковому наборі:

    V1__init_schema.sql

    V2__init_indexes.sql

    V3__seed_reference_data.sql

  2. Кладемо ці файли в src/main/resources/db/migration і додаємо flyway-core, щоб міграції стали частиною звичайного шляху запуску.
  3. Перемикаємо Hibernate на ddl-auto: validate, щоб схема змінювалася тільки через міграції, а не через ddl-auto=update.
  4. Для навчального переходу беремо чисту PostgreSQL-базу, а не намагаємося поєднати новий Flyway з уже накопиченою локальною схемою.
  5. Запускаємо застосунок: спочатку Flyway застосовує відсутні версії, потім Hibernate перевіряє мапінг.
  6. Перевіряємо, що застосунок стартував і що 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 знайшли їх без додаткових налаштувань.

У нашому проєкті після фіксації початкового набору V1V3 структура зазвичай виглядає так:

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-файл, який комітиться разом із кодом.

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