Розробка міграцій бази даних для веб-додатків
Міграції — керований спосіб змінювати схему БД разом із кодом. Без них зміни схеми розгортаються вручну, втрачаються в Slack або вызивают інциденти в продакшені.
Інструменти
Вибір залежить від вашого стеку:
| Інструмент | Стек | Формат |
|---|---|---|
| Flyway | Java, будь-який | SQL |
| Liquibase | Java, будь-який | XML/YAML/SQL |
| Alembic | Python/SQLAlchemy | Python |
| golang-migrate | Go, будь-який | SQL |
| Laravel Migrations | PHP/Laravel | PHP |
| Rails Migrations | Ruby/Rails | Ruby |
| Knex | Node.js | JS |
| Prisma Migrate | Node.js/TypeScript | Prisma schema |
Універсальний варіант — golang-migrate або Flyway з чистим SQL: не залежать від ORM, працюють з CI без додатку, міграції читаються без знання специфіки ORM.
Принципи написання міграцій
Кожна міграція — атомарна і оборотна. Якщо не можна написати коректний down, це сигнал, що міграція робить занадто багато.
Міграції в production не редагуються. Якщо помилка застосована — напишіть нову міграцію.
Немає data migrations в schema migrations. Перенесення даних — окремий скрипт.
Приклад із golang-migrate
migrate create -ext sql -dir db/migrations -seq add_search_vector_to_products
Створює два файли: 000003_add_search_vector_to_products.up.sql та ...down.sql.
-- 000003_add_search_vector_to_products.up.sql
BEGIN;
ALTER TABLE products
ADD COLUMN IF NOT EXISTS search_vector TSVECTOR;
UPDATE products
SET search_vector = to_tsvector('russian', coalesce(title, '') || ' ' || coalesce(description, ''));
CREATE INDEX CONCURRENTLY idx_products_search ON products USING GIN (search_vector);
CREATE OR REPLACE FUNCTION products_search_vector_update() RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector := to_tsvector('russian',
coalesce(NEW.title, '') || ' ' || coalesce(NEW.description, '')
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER products_search_vector_trigger
BEFORE INSERT OR UPDATE ON products
FOR EACH ROW EXECUTE FUNCTION products_search_vector_update();
COMMIT;
-- 000003_add_search_vector_to_products.down.sql
BEGIN;
DROP TRIGGER IF EXISTS products_search_vector_trigger ON products;
DROP FUNCTION IF EXISTS products_search_vector_update();
DROP INDEX IF EXISTS idx_products_search;
ALTER TABLE products DROP COLUMN IF EXISTS search_vector;
COMMIT;
CREATE INDEX CONCURRENTLY не можна виконувати всередині транзакції. Для таких міграцій обертаємо в окремий крок без BEGIN/COMMIT, або використовуємо Flyway з executeInTransaction = false.
Міграції без простоїв
Головне правило: кожна міграція повинна бути сумісною з попередньою та наступною версією коду одночасно.
Тому що розгортання виглядає так: спочатку застосовується міграція, потім піднімаються нові інстанси додатку, старі поступово вимикаються. У цей момент обидва покоління коду працюють одночасно.
Додавання колонки:
-- Безпечно: nullable без DEFAULT або з константою
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- Небезпечно в старих PostgreSQL: NOT NULL з DEFAULT — переписування таблиці
-- Безпечно в PostgreSQL 11+: NOT NULL з DEFAULT константою (без переписування)
ALTER TABLE users ADD COLUMN is_verified BOOLEAN NOT NULL DEFAULT false;
Перейменування колонки — завжди в 3 етапи:
Deploy 1: додати нову колонку, код пише в обидві
Deploy 2: перенести дані, код читає з нової
Deploy 3: видалити старку колонку
Зміна типу колонки:
-- PostgreSQL 12+: миттєвий для varchar(n) -> varchar(m) коли m > n
-- переписування таблиці для більшості інших змін типу
-- Безпечний шлях: додати нову, скопіювати, перемкнути
ALTER TABLE products ADD COLUMN price_cents BIGINT;
UPDATE products SET price_cents = (price * 100)::BIGINT;
-- наступний deploy коду: читаємо price_cents
-- наступна міграція: DROP COLUMN price
Видалення колонки:
-- Спочатку код перестає читати/писати колонку (deploy)
-- Потім міграція
ALTER TABLE products DROP COLUMN old_field;
Версіонування та іменування
Добрий формат імені: {timestamp}_{verb}_{object}_{detail}.
20240315_001_create_users.sql
20240315_002_add_email_index_to_users.sql
20240320_001_add_status_to_products.sql
20240401_001_create_orders.sql
Версії на основі timestamp (як у Flyway) краще за sequence-based (Alembic --autogenerate) при паралельній роботі декількох розробників: немає конфліктів версій.
CI/CD інтеграція
# .github/workflows/deploy.yml (фрагмент)
- name: Run migrations
run: |
migrate -path db/migrations \
-database "$DATABASE_URL" \
-verbose up
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Deploy application
run: ./deploy.sh
Міграції застосовуються до розгортання нового коду. Откат: migrate down N — откатити N останніх міграцій.
Flyway для Java/будь-якого стеку
<!-- pom.xml -->
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<configuration>
<url>${DB_URL}</url>
<user>${DB_USER}</user>
<password>${DB_PASSWORD}</password>
<locations>classpath:db/migration</locations>
<outOfOrder>false</outOfOrder>
<validateOnMigrate>true</validateOnMigrate>
</configuration>
</plugin>
mvn flyway:migrate
mvn flyway:info # статус усіх міграцій
mvn flyway:validate # перевірити контрольні суми
Flyway зберігає контрольну суму кожної міграції в таблиці flyway_schema_history. Якщо файл змінився після застосування — validate видасть помилку.
Графіки
Налаштування інфраструктури міграцій (інструмент, іменування, CI-крок, процедура откату) для нового проекту: половина дня. Написання набору початкових міграцій для існуючої схеми (reverse engineering): 1 день.







