[Перевод] Как правильно мигрировать БД в продакшене с использованием Liquibase и Flyway

Новый перевод от команды Spring АйО расскажет вам о вызовах, которые ставит перед разработчиками создание скриптов миграций баз данных и их организация, особенно при работе с большими системами.

Статья также содержит полезные советы о преодолении этих вызовов и о приемах, которые могут облегчить жизнь команде программистов, работающих с миграциями баз данных.

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

В этой статье мы посмотрим на best practices, инструменты и стратегии для управления миграциями баз данных в крупных масштабах, помогающих вашим системам оставаться устойчивыми, эффективными и простыми в адаптации к новым условиям.

Введение

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

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

Однако, эффективное внедрение таких решений таит в себе дополнительные вызовы, особенно при работе с крупномасштабными системами. Давайте посмотрим поподробнее на некоторые из этих проблем и на способы их решения. 

Выполнение миграций баз данных при запуске приложения 

Многие фреймворки предлагают поддержку интеграции инструментов миграции баз данных «из коробки». Поведение по умолчанию для этих фреймворков — проверить, остались ли какие-либо невыполненные миграции и применить их во время запуска приложения.

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

  • Запуск миграций при каждом старте приложения, даже если необходимые изменения отсутствуют, добавляет ненужную дополнительную работу по проверке целостности контрольных сумм миграционного скрипта, что может замедлить инициализацию приложения.

  • В распределенных системах с многочисленными инстансами приложения одновременные попытки миграции могут привести к race condition, lock или конфликтам, потенциально вызывающим простой системы или несовместимые состояния.

  • Запуск миграций во время процедура запуска приложения тесно связывает изменения в схеме с деплойментом приложения, и в дальнейшем будет сложнее отвязать процесс выхода релиза от миграций. Это может усложнить rolling обновления, blue-green деплойменты и canary релизы.

Чтобы избежать этих проблем:

  • Запускайте миграции отдельно: выполняйте миграции как отдельный шаг перед деплойментом и всегда убеждайтесь в том, что они применились до запуска приложения. 

  • Контролируемое выполнение: используйте инструменты миграции баз данных, такие как Flyway или Liquibase с механизмами, предотвращающими многопоточную миграцию, убедившись в том, что только один инстанс применяет изменения.

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

Поддержка транзакционных DDL-запросов  в базе данных

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

Однако, это не всегда возможно, если база данных сама по себе не поддерживает транзакции для DDL инструкций, таких как CREATE TABLE, ALTER TABLE и т.д.

Например, PostgreSQL поддерживает транзакционный DDL, поэтому вы можете безопасно включать такие операции как CREATE TABLE, ALTER TABLE или DROP TABLE внутрь транзакций, позволяя вам группировать по нескольку изменений схемы вместе и откатывать их, если это необходимо. 

Однако, некоторые базы данных, например, MySQL, MariaDB и Oracle (до 12c) не поддерживают транзакции для DDL инструкций.

Скажем, у вас есть вот такой миграционный скрипт:

CREATE TABLE employees ( 
  id int not null,
  name varchar(100) not null,
  email varchar(200),
  primary key (id)
);

INSERT INTO employees(id, name, email) VALUES (1, null, 'emp1@gmail.com');

Когда вы пытаетесь применить эту миграцию в базе данных PostgreSQL, она выдаст ошибку, потому что null значение вставляется в non-nullable колонку. Поскольку PostgreSQL поддерживает транзакционные DDL-запросы, а миграция выполняется в рамках транзакций, создание таблицы также будет отменено.

Если вы попытаетесь применить ту же самую миграцию при использовании базы данных MySQL или Oracle, миграция выдаст ошибку при выполнении команды INSERT, но таблица employees останется созданной, поскольку не будет выполнен rollback для CREATE TABLE

Вы можете использоватьTestcontainers для тестирования миграций в более низкоуровневых окружениях (local, dev), чтобы убедиться в том, что миграционные скрипты валидны.

Миграции баз данных с обратной совместимостью 

Некоторые приложения могут требовать обратной совместимости своих API, чтобы поддерживать клиентские приложения, использующие как более старые, так и более новые версии. Миграции баз данных, обеспечивающие обратную совместимость, необходимы для минимизации числа критических проблем во время деплоймента, а также для того, чтобы приложение и база данных могли эволюционировать независимо друг от друга. Обратная совместимость гарантирует, что старые версии приложения все еще могут работать с обновленной схемой базы данных, позволяя безопасно разворачивать базу и проводить деплойменты без простоя серверов. 

Мы должны стремиться создавать обратно-совместимые миграции баз данных, следуя приведенным ниже правилам:

  • Прежде всего добавляем изменения, которые ничего не сломают: вводим только такие изменения, которые не ломают никакую существующую функциональность. Добавляя новые колонки, всегда добавляем их со значением по умолчанию или позволяем им быть nullable.

  • Избегаем деструктивных операций: стараемся не удалять колонки, таблицы или индексы, от которых текущее приложение может зависеть. Вместо удаления колонки помечаем ее как устаревшую (deprecated) и удаляем ее при более поздних миграциях. Не удаляем колонки или таблицы напрямую — вместо этого добавляем новые и избавляемся от старых поэтапно.

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

  • Используем представления (views) для логических изменений: чтобы уменьшить количество изменений в коде, предназначенных для поддержки старой и новой схемы, используем представления баз данных везде, где это возможно.

Использование многофазного подхода 

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

Фаза 1 — Безопасность в первую очередь, почистим потом: приоритезируем безопасность добавления новой функциональности в схему, чистка схемы от устаревших элементов может быть проведена позже. На этой стадии мы только добавляем таблицы и колонки, которые не ломают существующую логику приложения.

Фаза 2 — Временное хранение двойных записей: записываем данные одновременно в старые и новые таблицы/колонки, используя триггеры или код приложения.

Фаза 3 — Считывание из нового источника: обновляем код приложения под считывание из новых таблиц/колонок.

Фаза 4 — Чистка: удаляем триггеры или код приложения, который переносит данные из старыхтаблиц/колонок в новые таблицы/колонки. Удаляем устаревшие таблицы, колонки, индексы и т.д.

Всегда тестируйте миграции в окружениях QA/staging, прежде чем применить их к продакшен базе данных. Создайте стратегию тестирования для валидации правильной работы миграции баз данных со старой и новой версией кода приложения, используя автоматизированные тесты.

Структуризация миграций баз данных 

При структуризации миграционных скриптов Flyway следуйте best practices, чтобы гарантировать, что миграциями будет легко управлять и поддерживать их в понятном для пользователей виде по мере роста размера приложения.

1. Используйте один скрипт на одну новую функцию или изменение 

Для каждой новой возможности или исправления бага, для которого требуется изменение в схеме базы данных, создавайте один миграционный скрипт с «говорящим» именем файла. Не группируйте никак не связанные между собой изменения в одном скрипте, следите за тем, чтобы каждая миграция фокусировалась на чем-то одном и соответствовала принципу атомарности. Этот подход обеспечивает модульность миграций, уменьшает число конфликтов при слиянии изменений от разных команд и упрощает откат изменений и отладку.

Examples:

   V1.1__add_users_table.sql
   V1.2__add_index_on_email.sql
   V1.3__rename_column_lastname.sql

2. Придерживайтесь последовательной нумерации версий 

Flyway выполняет скрипты, опираясь на порядок версий, поэтому последовательная нумерация имеет принципиальное значение.

Для хотфиксов и параллельной разработки придерживайтесь следующих соглашений:

  • Используйте V3.1__hotfix_script.sql для незначительных изменений после V3.

  • Используйте timestamp для нумерации версий в дополнение к самим номерам, если ваша команда часто работает над миграциями несколькими группами параллельно. 

V20250127__1_add_disabled_column.sql
V20250127__2_add_index_on_status.sql
  • В качестве альтернативы вы можете резервировать блоки для отдельных команд/разрабатываемых возможностей (например. V1000–1099 для Команды A, V1100–1199 для Команды B).

3. Группируйте скрипты по версиям и разрабатываемым возможностям в папках

Хотя Flyway не требует, чтобы скрипты находились в разных подпапках, их использование помогает организовать миграции в больших проектах.

Вы можете группировать миграционные скрипты по версиям следующим образом:

migrations/
 ├── v1/
 │   ├── V1__create_initial_schema.sql
 │   ├── V1.1__add_roles_table.sql
 │   └── V1.2__alter_users_add_column.sql
 ├── v2/
 │   ├── V2__add_orders_table.sql
 │   └── V2.1__add_order_details_table.sql
 └── v3/
     └── V3__optimize_indexes.sql

Либо вы можете группировать их по разрабатываемым возможностям, как показано ниже:

migrations/
  ├── users/
  │   └── V1.1__add_users_table.sql
  ├── catalog/
  │   ├── V2__add_products_table.sql
  │   └── V2.1__update_products.sql
  └── orders/
      ├── V3__add_orders_table.sql
      └── V3.1__optimize_orders_index.sql

Объединение исторических миграций

С течением времени количество миграционных скриптов может существенно вырасти, а значит, создание базы данных по этим миграциям для целей разработки или тестирования будет занимать много времени.

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

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

V1__create_users_table.sql
V2__add_status_column.sql
V3__add_forgot_pwd_token_column.sql
V4__create_products_table.sql
...
...
V10__add_index_on_status.sql

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

Используя Flyway, мы можем создать baseline миграционный скрипт, который создает целевое состояние схемы базы данных, с именем файла B10__baseline.sql. Затем вы можете добавлять дальнейшие скрипты миграции следующим образом:

V1__create_users_table.sql
V2__add_status_column.sql
V3__add_forgot_pwd_token_column.sql
V4__create_products_table.sql
...
...
V10__add_index_on_status.sql
B10__baseline.sql
V11__create_inventory_table.sql
V12__create_promotions_table.sql

В условиях наличия всех этих миграций, если вы запустите Flyway миграцию на новой базе данных, выполнятся только миграции  B10, V11 и V12. Таким образом схема будет создана быстрее, потому что не понадобится запускать скрипты миграции, идущие до baseline версии.

Когда Flyway миграция применяется к существующей базе данных (production или staging), Flyway проигнорирует baseline миграцию и применит скрипты миграции V11 и V12.

Если вы хотите узнать больше об использовании baseline миграций Flyway для объединения исторических миграций, ознакомьтесь со статьей Flyway Baseline миграция без лишних слов: Что это и зачем нужно.

Тестирование миграционных скриптов

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

Если ваши инструменты миграции базы данных поддерживают прогоны в режиме dry run для получения консолидированной информации обо всех запланированных изменениях без их реального применения, было бы полезно, если бы DBA их проверили.

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

Использование профессиональной поддержки инструментария 

Хотя мы можем писать миграции для баз данных вручную, этот процесс может быть весьма утомительным, поскольку придется писать многочисленные SQL запросы на разных диалектах баз данных и обеспечивать обратную совместимость. Использование профессиональных инструментов, таких как IntelliJ IDEA, может помочь оптимизировать эти задачи, уменьшить количество ошибок и увеличить продуктивность.

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

Комментарий редакции Spring АйО

Демонстрируемая в данной статьей функциональность доступна только в IntelliJ IDEA Ultimate, использование которой на территории Российской Федерации связано с определенными ограничениями. Однако все продемонстрированные ниже возможности также предоставляет плагин Amplicode, обзор на который мы делали во второй статье из цикла «Как жить без IntelliJ IDEA».

ee20da5a3d4bcb41f5c95091abe9baaa.gif

Кроме того, IntelliJ IDEA предоставляет визуальные подсказки, чтобы помочь вам понять суть изменений в базе данных, тем самым упрощая отслеживание и управление миграциями.

afc62567ffd255861e229ae03239dce5.webp

Каждый тип изменений кодируется цветом в соответствии с уровнем опасности: зеленый для SAFE (безопасный), желтый для CAUTION (внимание) и красный для DANGER (опасность). 

  • Операции уровня SAFE не могут вызывать потери данных никоим образом. Например, добавление колонки не влияет на существующие данные. 

  • Операции, помеченные как CAUTION, в целом безопасны, но требуют вашего внимания. Например, добавление ограничения NOT NULL может не отработать корректно, если в колонке есть null значения. 

  • Операции уровня DANGER могут вызвать потерю данных, как например удаление колонки или изменение типа данных.

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

Заключение 

Мы рассмотрели различные вызовы, которые бросает нам реальный мир при реализации миграций баз данных в крупных приложениях. Принятие многофазного подхода позволяет нам минимизировать критические проблемы и обеспечить обратную совместимость миграций. Кроме того, мы можем упростить сам процесс написания миграционных скриптов, если будем следовать рекомендованным соглашениям по именованию и организации скриптов по версиям или разрабатываемому функционалу. 

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

Хотя вы можете создавать миграционные скрипты вручную, это может отнимать много времени и вызывать появление ошибок. Вы можете использовать IntelliJ IDEA для создания миграционных скриптов в различных сценариях, например:

  • Создание миграционных скриптов по JPA сущностям.

  • Генерация новых миграций после обновления JPA сущностей дельтой изменений по существующим базам данных.

c164b24fe67857325369e7d0e25ab10e.png

Регистрируйтесь на главную конференцию про Spring на русском языке от сообщества Spring АйО! В мероприятии примут участие не только наши эксперты, но и приглашенные лидеры индустрии.

© Habrahabr.ru