Разработка миграций базы данных веб-приложения
Миграции — управляемый способ изменять схему БД вместе с кодом. Без них изменения схемы деплоятся вручную, теряются в слаке или вызывают инциденты в prod.
Инструменты
Выбор зависит от стека:
| Инструмент | Стек | Формат |
|---|---|---|
| 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.
Zero-downtime миграции
Главное правило: каждая миграция должна быть совместима с предыдущей и следующей версией кода одновременно.
Потому что деплой выглядит так: сначала применяется миграция, потом поднимаются новые инстансы приложения, старые постепенно снимаются. В этот момент оба поколения кода работают одновременно.
Добавление колонки:
-- Безопасно: nullable без DEFAULT или с константой
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- Небезопасно в старых PostgreSQL: NOT NULL с DEFAULT — table rewrite
-- Безопасно в PostgreSQL 11+: NOT NULL с DEFAULT константой (no rewrite)
ALTER TABLE users ADD COLUMN is_verified BOOLEAN NOT NULL DEFAULT false;
Переименование колонки — всегда в 3 этапа:
Деплой 1: добавить новую колонку, код пишет в обе
Деплой 2: перенести данные, код читает из новой
Деплой 3: удалить старую колонку
Изменение типа колонки:
-- PostgreSQL 12+: instant для varchar(n) -> varchar(m) при m > n
-- table rewrite для большинства других изменений типа
-- Безопасный путь: добавить новую, скопировать, переключить
ALTER TABLE products ADD COLUMN price_cents BIGINT;
UPDATE products SET price_cents = (price * 100)::BIGINT;
-- следующий деплой кода: читаем price_cents
-- следующая миграция: DROP COLUMN price
Удаление колонки:
-- Сначала код перестаёт читать/писать колонку (деплой)
-- Потом миграция
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-based версии (как в 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-шаг, rollback-процедура) для нового проекта: полдня. Написание набора начальных миграций для существующей схемы (reverse engineering): 1 день.







