Назад до блогу
8 жовтня 2026 6 хв читання

Патерн Expand and Contract для безпечних змін схеми бази даних

Зміна бізнес-вимог часто тягне за собою зміну наявної схеми бази даних. Проте вносити такі зміни складно: великий ризик зламати клієнтів бази даних — вебсервіси, serverless-функції тощо.

У цій статті познайомимося з патерном Expand and Contract («розширити і звузити»). Він дає змогу безпечно впроваджувати несумісні зміни (breaking changes) серією невеликих кроків, кожен з яких можна протестувати й відкотити.

Коли потрібна несумісна зміна

Уявіть, що ви розробляєте систему, у якій користувачі діляться відео з іншими людьми. Доменний шар (domain layer) такого проєкту, найімовірніше, міститиме сутність Video. Ось як може виглядати її максимально спрощена модель:

public class Video
{
    public int Id { get; set; }
    public string Title { get; set; }
    public bool IsPublic { get; set; }
}

Зверніть увагу на властивість IsPublic. Вона визначає, чи відео приватне (його бачить лише автор), чи публічне (його бачать усі).

Навколо доменної моделі Video може бути побудована, наприклад, така архітектура:

Початкова архітектура: обидва сервіси працюють із колонкою IsPublic

Згодом бізнес-вимоги змінюються: відео потрібні ще два статуси — MembersOnly і Unlisted.

Булева колонка більше не підходить. Її потрібно замінити колонкою PublicationStatus, яка зможе зберігати більше двох статусів.

Як впровадити цю зміну? Найпростіший підхід — внести потрібні зміни одразу в усі три компоненти:

  1. База даних. Написати скрипт, який додасть нову колонку, скопіює дані тощо.
  2. Writer. Оновити сервіс завантаження (uploader), щоб він записував дані в нову колонку.
  3. Reader. Оновити сервіс читання (reader), щоб він читав дані з нової колонки.

І останній, найважливіший крок — розгорнути всі ці зміни одночасно. Інакше ми ризикуємо зламати систему.

Проте в розподілених системах розгортання всього й одразу (big-bang deployment) часто не підходить: можливий простій системи, розгортання триває довше, а відкат у разі неочікуваних проблем стає складним. До того ж іноді ви просто не контролюєте графік розгортання якогось із компонентів.

Патерн Expand and Contract

Патерн Expand and Contract дає змогу змінювати інтерфейс поступово, не ламаючи систему. У нашому випадку інтерфейс, який ми змінюємо, — це схема бази даних. Проте патерн застосовний і до інших інтерфейсів: REST API, контрактів повідомлень у service bus, інтерфейсів у мовах програмування.

Патерн дає змогу перевести систему на новий інтерфейс невеликими кроками, які легко протестувати й відкотити. При цьому кожен крок зворотно сумісний (backward compatible) з рештою системи.

Розгляньмо, як це працює на практиці.

Фаза розширення (Expand)

Перший крок — розширити схему бази даних відповідно до нових бізнес-вимог. Додаємо колонку PublicationStatus, яка може зберігати ширший діапазон значень, ніж булева IsPublic.

Expand: додаємо колонку PublicationStatus, не чіпаючи IsPublic

Зверніть увагу: зміна має бути адитивною (additive), тобто лише додавати нове. Такі зміни за своєю природою зворотно сумісні з клієнтами.

Змінювати колонку IsPublic не можна, бо від неї залежать сервіси. Крім того, у скрипті бази даних колонці PublicationStatus потрібно задати значення за замовчуванням — наприклад, NULL або інше, яке має сенс для системи.

Далі оновлюємо сервіс завантаження (uploader). Він має:

  • почати записувати дані в нову схему (колонку PublicationStatus);
  • продовжувати записувати дані в стару схему (колонку IsPublic).
Uploader записує дані і в стару, і в нову колонку

Перехідна фаза (Transition)

Тепер writer записує метадані відео і в стару, і в нову колонку.

Проте значення в колонці PublicationStatus є лише в нових записах. Старі записи досі містять у ній значення за замовчуванням:

Старі записи ще не мають значення в PublicationStatus

Тому наступний крок — перенести дані старих записів із колонки IsPublic у колонку PublicationStatus. Наприклад, для цього можна додати код у post-deployment скрипт проєкту.

У нашому випадку правило конвертації просте: false перетворюється на Private, а true — на Public. Виконуючи таку міграцію, важливо переконатися, що правила конвертації зберігають початковий зміст значень зі старої схеми.

Тепер колонку PublicationStatus заповнено для всіх записів. Коли зміни схеми ретельно протестовано, час оновити сервіси читання (readers), щоб вони читали дані з колонки PublicationStatus:

Reader читає PublicationStatus замість IsPublic

Зверніть увагу, що reader більше не читає колонку IsPublic. Проте, оновлюючи readers у своєму проєкті, варто розглянути можливість якийсь час читати дані і зі старої, і з нової схеми — наприклад, доки ви не переведете на нову схему ще й контракти API.

Фаза звуження (Contract)

Оскільки reader тепер читає дані лише з нової схеми, сервісу завантаження більше не потрібно записувати дані в стару:

Uploader більше не записує IsPublic: колонкою ніхто не користується

Як бачите, uploader більше не записує колонку IsPublic. Після розгортання цієї зміни стара схема системі більше не потрібна.

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

Contract: видаляємо колонку IsPublic

Також переконайтеся, що з репозиторію видалено артефакти коду, пов’язані зі старою схемою, — наприклад, властивості доменних моделей.

На цьому все!

Переваги й недоліки Expand and Contract

Розгляньмо основні переваги й недоліки патерну.

Переваги

  • Зворотна сумісність (backward compatibility). Зміни бази даних зворотно сумісні з її клієнтами. Клієнти продовжують працювати без простою і поступово адаптуються до нових змін.
  • Тестованість (testability). Міграція на нову схему відбувається невеликими інкрементальними кроками, тож після кожного кроку можна провести ретельне тестування. А якщо щось пішло не так, можна відкотитися до попереднього кроку й переробити його.
  • Розгортання (deployment). Не потрібно розгортати всі задіяні компоненти синхронно. Кожен компонент можна розгортати за власним графіком.

Недоліки

  • Зусилля (effort). Реалізація патерну потребує багатьох скоординованих кроків і серії скоординованих розгортань, доки функціональність не буде повністю випущена.
  • Супровід (maintenance). Витрати на супровід вищі: певний час доводиться підтримувати і стару, і нову схеми та весь пов’язаний із ними код.
  • Залишки (leftovers). Є ризик, що фазу Contract так і не завершать: writer продовжить записувати дані в стару схему, а колонка в базі даних разом із пов’язаними артефактами коду залишиться невидаленою.

Висновок

Ми розглянули, як застосувати патерн Expand and Contract, щоб безпечно перевести систему на нову схему бази даних серією невеликих інкрементальних кроків.

Це переклад моєї англомовної статті «Expand and Contract Pattern for Safe Database Schema Changes», опублікованої на Medium у січні 2024 року.

ЩО ВИВЧАТИ ДАЛІ