Flyway — отличный инструмент для управления миграцией баз данных. Несколько лет назад Мартин Фаулер описал Эволюционный дизайн базы данных, и эта идея до сих пор актуальна.

Однако у Flyway есть небольшая оговорка, когда вы работаете над одним и тем же проектом в большой команде разработчиков. В этой статье я описываю потенциальную проблему, связанную с неупорядоченными миграциями, как ее исправить и как вы можете проверить шаблон имени миграции во время сборки проекта.

Всю настройку проекта вы можете найти в этом репозитории.

Проблема слияния вне порядка

Предположим, у нас есть 1 миграция V1__create_user.sql. Посмотрите на DDL ниже.

CREATE TABLE users
(
    id       BIGSERIAL PRIMARY KEY,
    username VARCHAR(200) NOT NULL,
    age      INTEGER      NOT NULL
)

Также в разных ветках Git работают два разработчика, Боб и Катя. Задача Боба требует, чтобы он добавил user_group таблицу. Он создает миграцию V2__create_user_group.sql. Пока Кейт хочет создать таблицу role и определяет новое имя миграции как V3__create_role.sql. Эти задачи не пересекаются и не блокируют друг друга. Разработчики могут успешно выполнять их, не дожидаясь, пока другие создадут запрос на вытягивание. В любом случае проблема может быть.

Предположим, что каждое слияние с веткой main приводит к развертыванию новой версии на стенде UAT. Кейт выполнила задание раньше, чем Боб. Итак, новый порядок миграции:

  1. V1__create_user.sql
  2. V3__create_role.sql

Что происходит, когда Боб объединяет свой запрос на извлечение? Новая миграция должна происходить между существующими. К сожалению, это приведет к ошибке во время выполнения миграции.

Validate failed: Migrations have failed validation
Detected resolved migration not applied to database: 2.

По умолчанию Flyway не позволяет добавлять новые миграции между существующими. Итак, нам нужно решить две проблемы:

  1. Flyway должен успешно выполнять новые миграции, даже если они находятся между текущими. Мы не хотим заставлять одного разработчика ждать, пока другой завершит свою работу.
  2. Для миграции требуется уникальный идентификатор (в данном случае 1, 2 и т. д.). Таким образом, каждый разработчик должен иметь возможность назначить уникальный идентификатор миграции. Кроме того, не должно быть дополнительных коллабораций, потому что это замедляет разработку и требует единого источника правды, содержащего все существующие и приобретенные идентификаторы миграций. Поэтому id должен быть децентрализован.

Стратегия именования миграций

Как вы можете догадаться, обычного возрастающего числа в качестве идентификатора недостаточно. Вместо этого я рекомендую вам применить этот шаблон:

V{CURRENT_DATE}.{TASK_ID}__{DESCRIPTION}.sql

Шаблон именования миграции является составным и состоит из нескольких блоков:

  1. {CURRENT_DATE} — это дата, когда разработчик добавил миграцию с шаблоном YYYY.MM.DD.
  2. {TASK_ID} — это уникальный идентификатор задачи, которая требует создания новой миграции. Это может быть задача Jira, Trello, YouTrack и так далее.
  3. {DESCRIPTION} — это описание, разъясняющее цель миграции.

Например, вот как может выглядеть создание новой таблицы users с новыми правилами именования миграции:

V2023.01.12.4343__create_users_table.sql

Это означает, что миграция была добавлена ​​12 января 2023 года, и соответствующая задача имеет идентификатор 4343.

Итак, теперь каждая миграция имеет уникальный идентификатор, потому что каждый разработчик работает над отдельной задачей в конкретной ветке.

Включение выполнения вне очереди

В любом случае, новый шаблон именования не решает проблему слияния неупорядоченных пулл-реквестов. К счастью, все, что вам нужно сделать, это включить параметр flyway.outOfOrder.

Давайте повторим случай с Бобом и Кейт, описанный ранее. Кейт объединила запрос на вытягивание, и порядок миграции таков:

  1. V2023.01.12.4343__create_users_table.sql
  2. V2023.01.14.4344__create_role.sql

Когда Боб объединяет свой запрос на извлечение, порядок меняется на:

  1. V2023.01.12.4343__create_users_table.sql
  2. V2023.01.13.4345__create_users_group_table.sql
  3. V2023.01.14.4344__create_role.sql

Если вы включите параметр flyway.outOfOrder, выполнение завершится без ошибок, и Flyway успешно создаст все таблицы.

На самом деле разработчики теперь могут объединять запросы на вытягивание в любом порядке, не беспокоясь о проблемах с порядком миграции. Однако это работает только в том случае, если все следуют установленным правилам именования. Вы можете проверить это во время код-ревью, но это неэффективно. Есть вероятность пропустить опечатку. Нам нужен подход, который будет автоматически проверять правильность шаблона именования.

Автоматизация проверки

Я показываю вам способ автоматизации с помощью Gradle, но идея остается той же для любого другого инструмента сборки.

Посмотрите на задачу Gradle, выполняющую проверку имен миграции.

def migrationExclusions = [/* migration names exclusions */]

task validateFlywayMigrations {
    def migrationPattern = "^V\\d{4}\\.\\d{2}\\.\\d{2}\\.\\d+__[a-z_]+\\.sql\$"
    def datePattern = Pattern.compile("\\d{4}\\.\\d{2}\\.\\d{2}")

    doLast {
        for (def file in fileTree('src/main/resources/db/migration')) {
            final String migrationName = file.getName()
            if (!file.isFile() || migrationExclusions.contains(migrationName)) {
                continue
            }
            if (!migrationName.matches(migrationPattern)) {
                throw new GradleException("Migration '$migrationName' does not match pattern '$migrationPattern'")
            }
            def matcher = datePattern.matcher(migrationName)
            if (matcher.find()) {
                def date = matcher.group()
                try {
                    LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy.MM.dd"))
                } catch (DateTimeParseException e) {
                    throw new GradleException(
                            "Migration '$migrationName' has invalid date value. Couldn't be parsed with pattern 'yyyy.MM.dd'",
                            e
                    )
                }
            } else {
                throw new GradleException("Migration '$migrationName' has no date by pattern '$datePattern'")
            }
        }
    }
}

compileJava.dependsOn validateFlywayMigrations

Идея тривиальна. Мы читаем все файлы из каталога src/main/resources/db/migration и проверяем, что имя удовлетворяет шаблону RegExp ^V\d{4}\.\d{2}\.\d{2}\.\d+__[a-z_]+\.sql\$. Если это так, когда мы подтверждаем, что шаблон \d{4}\.\d{2}\.\d{2} содержит правильную дату, а не просто случайную строку, такую ​​как 3033.45.98. Если какая-либо проверка не проходит, возникает исключение, и мы получаем ненулевой код результата, что приводит к сбою команды оболочки.

Из-за предложения dependsOn validateFlywayMigrations запускается автоматически прямо перед компиляцией кода.

Поле migrationExclusions полезно при применении новых правил именования к существующему проекту. Вы не можете изменить имена существующих миграций. Таким образом, вы игнорируете их в процессе проверки.

Заключение

В результате разработчикам не нужно беспокоиться ни об уникальности имён миграций Flyway, ни о порядке слияния пулл-реквестов. Это сделает программирование более приятным и менее напряженным. Расскажите свою историю в комментариях. Были ли у вас подобные проблемы в вашем проекте? Если да, то как вы решили проблемы?

Это все, что я хотел рассказать о нейминге миграции Flyway. Спасибо за прочтение!

Ресурсы

  1. Пролетная дорога
  2. Эволюционный дизайн базы данных
  3. Репозиторий с примерами кода
  4. Стенд УАТ
  5. Единый источник правды
  6. Децентрализованный идентификатор
  7. Параметр flyway.outOfOrder
  8. Грэдл
  9. РегЭксп