Добавление новых структур базы данных без остановки работающих приложений
У вас есть таблица users с колонкой full_name. Команда продукта хочет разделить имена на first_name и last_name для лучшей персонализации. Вам нужно внести это изменение без остановки приложения или поломки существующего функционала.
Очевидный подход — удалить full_name, добавить две новые колонки и обновить весь код разом. Но это требует согласованных деплоев, даунтайма и идеального плана отката, если что-то пойдет не так. На практике такие изменения часто приводят к ночным откатам и злым пользователям.
Есть более безопасный путь: сначала добавить новую структуру, не трогая старую. Позвольте старому и новому сосуществовать, пока вы не будете готовы к полному переключению.
Фаза Expand: Добавляем, не удаляя
Паттерн expand-contract начинается с максимально безопасного шага. Вы добавляете новые колонки, таблицы или ограничения в базу данных, ничего не меняя в том, что уже существует. Старые приложения, которые все еще работают со старой схемой, ничего не заметят. Новый код может сразу начать использовать новые структуры.
В этом суть фазы expand: вы вводите новые объекты базы данных, не изменяя и не удаляя существующие. Старая схема остается полностью работоспособной. Новая схема — это дополнение, а не замена.
Диаграмма ниже показывает состояние таблицы users до и после фазы expand, а также то, как старые и новые приложения взаимодействуют со схемой.
Конкретный пример
Возьмем таблицу users с колонкой full_name. В фазе expand вы добавляете две новые колонки:
Вот SQL для добавления новых колонок:
ALTER TABLE users ADD COLUMN first_name VARCHAR(100) NULL;
ALTER TABLE users ADD COLUMN last_name VARCHAR(100) NULL;
ALTER TABLE users ADD COLUMN first_name VARCHAR(100) NULL;
ALTER TABLE users ADD COLUMN last_name VARCHAR(100) NULL;
Колонка full_name остается как есть. Старые приложения, которые читают full_name, продолжают работать без каких-либо изменений кода. Новые приложения могут начать писать в first_name и last_name, одновременно читая full_name для обратной совместимости.
Никакого даунтайма. Никаких согласованных релизов. Никакого риска сломать существующие запросы.
Критическое правило: новые колонки должны быть опциональными
Самая распространенная ошибка в фазе expand — добавление новой колонки с NOT NULL и без значения по умолчанию. Это немедленно ломает любое приложение, которое вставляет строки без указания новой колонки. Старые приложения, которые не знают о существовании колонки, будут падать при каждой вставке.
Правило простое: каждая новая колонка должна быть nullable или иметь разумное значение по умолчанию. Если вам нужно ограничение NOT NULL, сначала добавьте колонку как nullable, заполните данные, а затем добавьте ограничение в более поздней фазе. Не заставляйте все существующие приложения одновременно менять свои INSERT-запросы.
Та же логика применима к новым таблицам. Новая таблица не меняет ни одну существующую структуру. Старым приложениям никогда не нужно знать о ее существовании. Новые приложения могут сразу начать в нее писать. Конфликта нет, потому что ничего не изменилось в таблицах, которые они уже используют.
Безопасная работа с ограничениями
Ограничения требуют особой осторожности во время фазы expand. Если вы добавляете ограничение UNIQUE на новую колонку, убедитесь, что существующие данные его не нарушают. Если вы добавляете FOREIGN KEY, убедитесь, что все существующие строки ссылаются на валидные родительские строки.
Для ограничений CHECK проверьте, что условие не конфликтует с существующими данными. Некоторые базы данных поддерживают опцию NOT VALID, которая применяет ограничение только к новым строкам, позволяя вам проверить существующие данные отдельно позже. Это полезно, когда вы не уверены в качестве данных в старых строках.
Принцип тот же: не вводите ограничение, которое не сработает на существующих данных. Если вы не можете этого гарантировать, отложите ограничение или добавьте его так, чтобы оно не блокировало запись.
Важность именования
Новые колонки и таблицы должны иметь понятные имена, отличающие их от старой структуры. Избегайте имен вроде name_new, temp_name или name_v2. Они создают путаницу во время фазы contract, когда нужно знать, какая структура является канонической.
Используйте имена, описывающие фактические данные. first_name и last_name лучше, чем name_split_1 и name_split_2. Хорошие имена облегчают переход для всех, кто впоследствии будет работать со схемой.
Что не требуется в фазе Expand
Фаза expand не требует никаких изменений кода в старых приложениях. Они продолжают использовать ту же схему, те же запросы и ту же логику. Именно это делает фазу expand безопасной для выполнения в любое время, даже в пиковые часы работы продакшена.
Никакого даунтайма. Никаких перезапусков приложений. Никаких внезапных ошибок запросов из-за того, что колонка исчезла. Единственное изменение — в схеме базы данных, и это изменение является чисто аддитивным.
Когда фаза Expand завершена
Фаза expand завершается, когда новые структуры существуют в базе данных и готовы к использованию. Старые приложения все еще используют старые структуры. Новые приложения могут начать использовать новые структуры. Оба пути работают одновременно.
На этом этапе у вас есть база данных, поддерживающая две версии схемы. Это основа для следующего шага: постепенной миграции приложений на новые структуры без поломок.
Практический чек-лист для фазы Expand
- Новые колонки nullable или имеют значение по умолчанию
- Новые таблицы не изменяют существующие структуры таблиц
- Новые ограничения не конфликтуют с существующими данными
- Имена четко отличают новые структуры от старых
- Старые приложения продолжают работать без изменений кода
- Новые приложения могут сразу начать использовать новые структуры
Вывод
Фаза expand — это самое безопасное изменение базы данных, которое вы можете сделать, потому что оно только добавляет. Никакого удаления, никаких изменений, никакого риска сломать работающие приложения. Добавляйте новые колонки как nullable, оставляйте старые колонки нетронутыми и позволяйте обеим схемам сосуществовать. Это дает вам свободу мигрировать приложения в своем темпе, без необходимости координировать релиз "большим взрывом" или планировать даунтайм.