Загрузка
Все статьи

Flyway vs Liquibase — миграции БД в Kotlin-проекте

Выбор инструмента миграций — кажется мелочью, пока не нужно откатить изменение в продакшене. Flyway — простой SQL-first подход. Liquibase — XML/YAML/JSON с абстракциями и rollback. Мы использовали оба в разных проектах и набили шишки на каждом. Рассказываю, в каком случае что выбрать и как не потерять данные при rollback.

Начинал я с Flyway, потому что он проще — кинул SQL-файлы в папку, всё заработало. Но через полгода на проде понадобилось откатить колонку, и я обнаружил, что у Flyway нет встроенного rollback. Пришлось писать SQL-скрипты вручную и молиться, что ничего не упадёт. На следующем проекте мы выбрали Liquibase — и набили другие шишки. Рассказываю, что из этого вышло.

Flyway в Kotlin/Spring Boot проекте

Подключение Flyway к Spring Boot — буквально три строки в build.gradle.kts:

dependencies {
    implementation("org.flywaydb:flyway-core")
    implementation("org.flywaydb:flyway-database-postgresql")
    runtimeOnly("org.postgresql:postgresql")
}

Миграции кладём в src/main/resources/db/migration. Нейминг строгий: V1__create_users_table.sql, V2__add_email_column.sql. Порядок определяется версионным префиксом, и Flyway применит их последовательно.

-- V1__create_users_table.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    username VARCHAR(255) NOT NULL UNIQUE,
    email VARCHAR(255),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- V2__add_email_unique.sql
ALTER TABLE users ADD CONSTRAINT uq_users_email UNIQUE (email);

В application.yml минимальная конфигурация:

spring:
  flyway:
    enabled: true
    locations: classpath:db/migration
    baseline-on-migrate: true
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: postgres
    password: ${DB_PASSWORD}

Плюсы Flyway очевидны: всё, что ты пишешь — это чистый SQL. Нет абстракций, нет магии. Открыл файл — видишь, что будет выполнено. Для команды из 3-5 backend-разработчиков, которые уверенно пишут SQL, это идеальный вариант. Но есть нюансы.

Проблема rollback в Flyway

Flyway Community не поддерживает автоматический откат. Да, есть Flyway Teams с командой flyway undo, но это платно. В комьюнити-версии у тебя два пути: либо писать undo-миграции вручную с префиксом U, либо просто писать новые миграции, которые «чинят» предыдущую.

На проде мы однажды добавили колонку phone_number с NOT NULL без дефолта. Миграция упала на таблице с миллионом строк. Откатить нечем. Пришлось руками коннектиться к БД и удалять колонку. После этого мы завели правило: все новые колонки — либо с дефолтом, либо через два шага — сначала nullable, потом fill + alter.

Liquibase: когда абстракция помогает

На втором проекте выбрали Liquibase, потому что у нас было три окружения: PostgreSQL в проде, MySQL на стейдже и H2 для тестов. Написание отдельных SQL-файлов для каждой СУБД превратилось бы в ад.

Подключение в build.gradle.kts:

dependencies {
    implementation("org.liquibase:liquibase-core")
}

А вот changelog уже в YAML-формате:

databaseChangeLog:
  - changeSet:
      id: 1
      author: artem
      changes:
        - createTable:
            tableName: orders
            columns:
              - column:
                  name: id
                  type: BIGINT
                  autoIncrement: true
                  constraints:
                    primaryKey: true
              - column:
                  name: status
                  type: VARCHAR(50)
                  defaultValue: "NEW"
                  constraints:
                    nullable: false
      rollback:
        - dropTable:
            tableName: orders

Обратите внимание на блок rollback — он описывает, как откатить изменение. Liquibase может сгенерировать SQL для отката автоматически или выполнить его по команде.

Конфигурация в application.yml:

spring:
  liquibase:
    enabled: true
    change-log: classpath:db/changelog/master.yaml
    contexts: dev

Что мы поняли на практике

Вот критерии, по которым мы сейчас выбираем:

Выбирайте Flyway, если: команда уверенно пишет SQL, нет необходимости поддерживать несколько СУБД, rollback делается через новые миграции (а не через undo), и вы хотите максимальную простоту. Flyway быстрее стартует, проще в дебаге, и SQL-файлы читает любой разработчик.

Выбирайте Liquibase, если: нужна поддержка нескольких СУБД, требуются автоматические rollback, в команде есть junior-разработчики которым опасно давать прямой SQL, или вы работаете в корпоративной среде с обязательным код-ревью миграций. Liquibase также круто интегрируется с Liquibase Hub для отслеживания статуса миграций.

Лично я сейчас по умолчанию беру Flyway для новых проектов на PostgreSQL. Но если вижу, что проект растёт и будут сложные миграции с data-трансформациями — пересаживаюсь на Liquibase. Перенос с Flyway на Liquibase болезненный, так что лучше определиться в начале.

Антипаттерны, которые мы встречали

Первый — хранение данных в миграциях. Когда кто-то пишет INSERT INTO ... VALUES внутри миграции для «начального наполнения» справочников. Это норма для статических данных, но у нас коллега закоммитил 5000 строк INSERT для каталога товаров. Миграция стала тормозить на каждом деплое. Для seed-данных лучше использовать отдельный механизм — Liquibase contexts или Flyway repeatable migrations.

Второй — изменение уже применённой миграции. Это ломает checksum и Flyway, и Liquibase. Всегда создавайте новую миграцию. Если нужно поправить предыдущую — делайте это через новую, не трогайте старую.

Третий — отсутствие IF EXISTS и IF NOT EXISTS в SQL. Да, Flyway отслеживает, какие миграции применены, но при ручном вмешательстве в БД (а это бывает) отсутствие проверок приводит к ошибкам при повторном запуске.

Поделиться статьёй

Комментарии

0
0 / 2000

Пока нет комментариев

Будьте первым, кто оставит комментарий к этой статье

Читайте также

Micronaut vs Spring Boot — лёгкий фреймворк для Kotlin-микросервисов

Сравниваю Micronaut и Spring Boot для Kotlin-микросервисов: стартовое время, потребление памяти, холодный старт и реальный опыт миграции одного из сервисов.

12 июня 2026 11 мин

Kotlin: sealed interfaces и when-выражения — паттерн-матчинг как в Rust

Как sealed interfaces из Kotlin 1.5+ вместе с when-выражениями дают мощный паттерн-матчинг, похожий на Rust enum+match, и почему мы перешли на ADT для доменных событий.

11 июня 2026 9 мин

Kotlinx Serialization + Protobuf — быстрая сериализация для микросервисов

Разбираюсь с накладными расходами JSON-сериализации при высоких нагрузках и показываю, как интегрировать Protobuf с kotlinx-serialization в микросервисной архитектуре на Kotlin.

10 июня 2026 10 мин