Когда миграции базы данных ломаются: почему roll-forward лучше rollback

Вы только что развернули миграцию базы данных, которая добавила столбец phone_number в таблицу users. Миграция выполнилась успешно. Затем ваша команда поняла, что код приложения, использующий этот столбец, еще не развернут. Теперь каждая регистрация нового пользователя завершается ошибкой, потому что старый код пытается вставить строку без указания значения для нового столбца NOT NULL.

Ваша рабочая система сломана. Что делать?

Большинство команд инстинктивно тянутся к down-миграции — скрипту, который отменяет изменения и удаляет столбец. Но этот инстинкт может нанести больше вреда, чем исходная проблема. Есть лучший подход: roll-forward.

Проблема с down-миграциями

Down-миграции выглядят хорошо на бумаге. Вы пишете up-миграцию, которая добавляет столбец, и down-миграцию, которая его удаляет. Если что-то пошло не так, вы запускаете down-миграцию, и всё возвращается в исходное состояние.

На практике down-миграции опасны по нескольким причинам.

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

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

В-третьих, down-миграции редко тестируются. Команды пишут их как дополнение, часто с ошибками, которые проявляются только во время реальной аварии. Запуск непроверенного скрипта в продакшене во время сбоя — верный способ увеличить время простоя.

Что такое roll-forward?

Roll-forward — это стратегия, при которой вы никогда не отменяете миграцию. Вместо этого, когда миграция вызывает проблемы, вы пишете новую миграцию, которая исправляет проблему. База данных движется вперед к исправленному состоянию, а не назад к предыдущему.

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

Вот как выглядит эта исправляющая миграция на SQL:

-- version_002: исправление ограничения phone_number
-- Эта миграция делает phone_number nullable, чтобы старый код приложения
-- мог вставлять строки без указания значения.
ALTER TABLE users
ALTER COLUMN phone_number DROP NOT NULL;

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

Почему команды предпочитают roll-forward

Самое большое преимущество — нулевая потеря данных. Если ваша первая миграция сохранила 500 номеров телефонов до того, как вы обнаружили проблему, эти номера переживут исправление. Down-миграция удалила бы их навсегда.

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

Этот подход отражает то, как команды обрабатывают ошибки в коде приложения. Когда ошибка попадает в продакшен, вы не откатываете всю кодовую базу до версии прошлой недели. Вы отправляете исправление. Миграции базы данных должны работать так же: исправление — это новая миграция, а не откат.

Когда roll-forward усложняется

Не каждое исправление roll-forward так же просто, как изменение ограничения столбца. Рассмотрим миграцию, которая изменила тип данных столбца с VARCHAR на INTEGER. Если преобразование обрезало или повредило существующие данные, ваша исправляющая миграция может потребовать:

  1. Добавить новый столбец с исходным типом данных
  2. Скопировать данные из поврежденного столбца, применяя преобразования для восстановления значений
  3. Обновить ссылки в коде приложения для использования нового столбца
  4. Удалить поврежденный столбец в последующей миграции

Это больше работы, чем простая down-миграция. Но это также безопаснее. Вы можете сначала запустить исправляющую миграцию в стейджинге, проверить логику восстановления данных и только затем применить её в продакшене. Down-миграция не дает такой страховки — она просто удаляет столбец и надеется на лучшее.

Ключевой вывод: roll-forward не требует от вас предсказывать все возможные сбои до развертывания. Вам просто нужна уверенность, что если что-то пойдет не так, ваша команда сможет написать исправление. Это смещает риск с предотвращения (которое невозможно сделать идеальным) на восстановление (навык, который можно тренировать).

Практический чек-лист для roll-forward

Прежде чем принять roll-forward как стратегию вашей команды, убедитесь, что эти практики внедрены:

Используйте это дерево решений, когда миграция вызывает проблемы:

flowchart TD A[Миграция вызывает проблему] --> B{Высокий риск потери данных?} B -->|Да| C[Roll-forward] B -->|Нет| D{Код и схема не синхронизированы?} D -->|Да| E[Roll-forward] D -->|Нет| F[Рассмотреть откат] C --> G[Написать исправляющую миграцию] E --> G F --> H[Запустить down-миграцию с осторожностью]
  • Каждая миграция должна быть обратима в теории, но не обязательно в коде. Понимайте, что сделала бы down-миграция, но не пишите её, если у вас нет конкретной причины для этого.
  • Тестируйте исправляющие миграции в стейджинге. Запустите исходную миграцию, воспроизведите сценарий сбоя, затем примените исправляющую миграцию. Проверьте целостность данных после.
  • Поддерживайте надежность трекера миграций. Никогда не изменяйте и не удаляйте записи миграций вручную. Трекер — это ваш аудиторский след для понимания того, что и когда изменилось.
  • Документируйте режим сбоя. Когда вы пишете исправляющую миграцию, добавьте комментарий, объясняющий, что пошло не так и почему исправление работает. Это поможет будущим членам команды, столкнувшимся с похожими паттернами.
  • Практикуйте сценарии roll-forward. Проводите ежеквартальные учения, где кто-то намеренно вносит плохую миграцию, а команда практикуется в написании и развертывании исправления в условиях ограниченного времени.

Вывод

Down-миграции — это ловушка. Они обещают простоту, но приводят к потере данных, головной боли с координацией и непроверенным аварийным скриптам. Roll-forward рассматривает изменения в базе данных как изменения в коде: когда что-то ломается, вы исправляете это движением вперед, а не назад.

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