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. Кейт выполнила задание раньше, чем Боб. Итак, новый порядок миграции:
V1__create_user.sql
V3__create_role.sql
Что происходит, когда Боб объединяет свой запрос на извлечение? Новая миграция должна происходить между существующими. К сожалению, это приведет к ошибке во время выполнения миграции.
Validate failed: Migrations have failed validation Detected resolved migration not applied to database: 2.
По умолчанию Flyway не позволяет добавлять новые миграции между существующими. Итак, нам нужно решить две проблемы:
- Flyway должен успешно выполнять новые миграции, даже если они находятся между текущими. Мы не хотим заставлять одного разработчика ждать, пока другой завершит свою работу.
- Для миграции требуется уникальный идентификатор (в данном случае
1
,2
и т. д.). Таким образом, каждый разработчик должен иметь возможность назначить уникальный идентификатор миграции. Кроме того, не должно быть дополнительных коллабораций, потому что это замедляет разработку и требует единого источника правды, содержащего все существующие и приобретенные идентификаторы миграций. Поэтому id должен быть децентрализован.
Стратегия именования миграций
Как вы можете догадаться, обычного возрастающего числа в качестве идентификатора недостаточно. Вместо этого я рекомендую вам применить этот шаблон:
V{CURRENT_DATE}.{TASK_ID}__{DESCRIPTION}.sql
Шаблон именования миграции является составным и состоит из нескольких блоков:
{CURRENT_DATE}
— это дата, когда разработчик добавил миграцию с шаблономYYYY.MM.DD
.{TASK_ID}
— это уникальный идентификатор задачи, которая требует создания новой миграции. Это может быть задача Jira, Trello, YouTrack и так далее.{DESCRIPTION}
— это описание, разъясняющее цель миграции.
Например, вот как может выглядеть создание новой таблицы users
с новыми правилами именования миграции:
V2023.01.12.4343__create_users_table.sql
Это означает, что миграция была добавлена 12 января 2023 года, и соответствующая задача имеет идентификатор 4343
.
Итак, теперь каждая миграция имеет уникальный идентификатор, потому что каждый разработчик работает над отдельной задачей в конкретной ветке.
Включение выполнения вне очереди
В любом случае, новый шаблон именования не решает проблему слияния неупорядоченных пулл-реквестов. К счастью, все, что вам нужно сделать, это включить параметр flyway.outOfOrder.
Давайте повторим случай с Бобом и Кейт, описанный ранее. Кейт объединила запрос на вытягивание, и порядок миграции таков:
V2023.01.12.4343__create_users_table.sql
V2023.01.14.4344__create_role.sql
Когда Боб объединяет свой запрос на извлечение, порядок меняется на:
V2023.01.12.4343__create_users_table.sql
V2023.01.13.4345__create_users_group_table.sql
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. Спасибо за прочтение!