Alembic: миграции БД, autogenerate, окружения
Alembic: миграции БД, autogenerate, окружения
В production схема БД меняется со временем: новая таблица, добавили колонку, переименовали индекс. Чтобы все окружения (dev, staging, prod) имели одинаковую схему, нужен migration tool. Для SQLAlchemy это Alembic. В этом уроке - setup, создание миграций, autogenerate, окружения и типичные грабли.
Зачем миграции
Без миграций нельзя:
- Применить изменения схемы согласованно на всех серверах
- Откатить изменения если что-то сломалось
- Сохранить историю изменений в git
- Понять что нужно для нового окружения
Альтернатива - писать SQL руками и помнить порядок - не масштабируется.
Установка и инициализация
pip install alembic
alembic init alembic
Создаёт структуру:
Конфигурация
В alembic.ini:
sqlalchemy.url = postgresql://user:pass@localhost/dbname
Лучше брать URL из env переменной. В env.py:
import os
from alembic import context
from myapp.db import Base
config = context.config
config.set_main_option("sqlalchemy.url", os.getenv("DATABASE_URL"))
target_metadata = Base.metadata # для autogenerate
target_metadata указывает на SQLAlchemy Base - Alembic будет сравнивать schema с моделями.
Создание миграции вручную
alembic revision -m "add users table"
Создаст файл в versions/:
# versions/abc123_add_users_table.py
revision = "abc123"
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"users",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column("name", sa.String(100)),
sa.Column("email", sa.String(255), unique=True),
)
def downgrade():
op.drop_table("users")
upgrade() - что сделать. downgrade() - как откатить (важно!). revision/down_revision - цепочка миграций. Прогонять alembic upgrade head лучше отдельным шагом деплоя - как это встроить в пайплайн, смотри в уроке про Docker и CI.
Autogenerate - main feature
alembic revision --autogenerate -m "add users table"
Alembic сравнит ваши SQLAlchemy модели с текущей схемой БД и сгенерирует diff. Это сильно ускоряет работу - не нужно писать SQL руками.
def upgrade():
op.create_table(
"users",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(length=100), nullable=False),
sa.Column("email", sa.String(length=255), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("email"),
)
op.create_index(op.f("ix_users_id"), "users", ["id"], unique=False)
Сгенерировано автоматически на основе моделей.
После autogenerate всегда:
- Прочитай сгенерированный файл
- Поправь если нужно (например, для rename добавь op.alter_column)
- Протестируй на копии данных перед prod
Применение миграций
# Применить все pending миграции
alembic upgrade head
# Применить конкретную миграцию (по revision)
alembic upgrade abc123
# Применить relative (одна вперёд)
alembic upgrade +1
# Откат на одну
alembic downgrade -1
# Откат всех
alembic downgrade base
# Откат до конкретной
alembic downgrade abc123
# Текущая ревизия
alembic current
# История
alembic history --verbose
Основные операции
# Таблицы
op.create_table("users", sa.Column(...), ...)
op.drop_table("users")
op.rename_table("old_name", "new_name")
# Колонки
op.add_column("users", sa.Column("age", sa.Integer))
op.drop_column("users", "age")
op.alter_column("users", "name", new_column_name="full_name")
op.alter_column("users", "email", type_=sa.String(500)) # change type
# Индексы
op.create_index("ix_users_email", "users", ["email"], unique=True)
op.drop_index("ix_users_email", table_name="users")
# Constraints
op.create_foreign_key("fk_orders_user", "orders", "users", ["user_id"], ["id"])
op.drop_constraint("fk_orders_user", "orders", type_="foreignkey")
# Raw SQL
op.execute("UPDATE users SET active = true WHERE created_at < '2026-01-01'")
Data migrations
Иногда нужно мигрировать данные, не схему:
def upgrade():
# Schema change
op.add_column("users", sa.Column("full_name", sa.String(200)))
# Data migration
from sqlalchemy.sql import table, column
users_table = table("users",
column("id", sa.Integer),
column("first_name", sa.String),
column("last_name", sa.String),
column("full_name", sa.String),
)
connection = op.get_bind()
result = connection.execute(users_table.select())
for row in result:
full = f"{row.first_name} {row.last_name}"
connection.execute(
users_table.update().where(users_table.c.id == row.id).values(full_name=full)
)
# Drop old columns
op.drop_column("users", "first_name")
op.drop_column("users", "last_name")
Для больших таблиц - batch processing. Один UPDATE на миллион строк может зависнуть.
Multiple environments
В env.py использовать env var для URL:
config.set_main_option("sqlalchemy.url", os.getenv("DATABASE_URL"))
Запуск на разных окружениях:
# dev
DATABASE_URL=postgresql://localhost/myapp_dev alembic upgrade head
# staging
DATABASE_URL=postgresql://stage.com/myapp_stage alembic upgrade head
# prod
DATABASE_URL=postgresql://prod.com/myapp_prod alembic upgrade head
В Docker/Kubernetes URL прокидывается через env. Лучше запускать миграции в init container или CI/CD pipeline перед deployment.
Branching и merge
Если в feature branch создана миграция, и в main тоже, при merge будут две миграции с одинаковым down_revision:
alembic heads # покажет несколько heads
alembic merge -m "merge branches" head1 head2
Создаёт merge миграцию, объединяющую branches. Это редко, но возможно при параллельной разработке.
Stamp - mark без выполнения
alembic stamp head
Помечает БД как up-to-date без выполнения миграций. Полезно когда:
- БД создана вручную (через create_all)
- Импортируешь existing DB в Alembic
- После manual fix хочешь синхронизировать metadata
Async support
С Alembic 1.6+ поддерживает async:
# env.py
from sqlalchemy.ext.asyncio import async_engine_from_config
def run_migrations_online():
connectable = async_engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
async def run():
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
asyncio.run(run())
Это сложнее sync setup. Если приложение FastAPI с async SQLAlchemy, всё равно лучше держать миграции в sync mode для простоты - они одноразовые операции.
CI/CD integration
# .github/workflows/deploy.yml
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -e .
- name: Apply migrations
env:
DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
run: alembic upgrade head
Перед каждым deployment - применить новые миграции. Если миграция fails - блокируется release.
Backward compatible migrations
Для zero-downtime deployments миграции должны быть backward compatible:
1. Добавление колонки (safe):
op.add_column("users", sa.Column("age", sa.Integer, nullable=True))
Старый код не упадёт - он не знает про новую колонку, она nullable.
2. Удаление колонки (опасно):
op.drop_column("users", "old_field")
Старый код может всё ещё писать в old_field - 500 ошибки. Стратегия:
- Релиз 1: код перестаёт писать в old_field
- Wait until все instances updated
- Релиз 2: миграция удаляет колонку
3. Изменение типа (опасно):
Аналогично. Лучше: добавь новую колонку, мигрируй данные, удали старую в следующем релизе.
Распространённые ошибки
1. Не writing downgrade
def downgrade():
pass # ОПАСНО - нельзя откатить
Всегда implement downgrade. Когда что-то сломалось в production, нужна возможность быстро откатиться.
2. Запускать миграции прямо в app
# В app startup
Base.metadata.create_all(engine) # БЕЗ Alembic
create_all не handles changes. И не version controlled. Используй Alembic с самого начала.
3. Изменять existing миграции
# Уже применённая миграция
def upgrade():
op.create_table(...)
op.create_table(...) # добавил после deployment
Если миграция уже применена в production - изменения не выполнятся. Создавай новую миграцию.
4. Большие миграции с downtime
def upgrade():
op.execute("UPDATE users SET ...") # на миллионах строк - часы downtime
Batch processing, индексы concurrently, schema changes online (depending on DB). Для PostgreSQL - CONCURRENTLY для индексов, pg_repack для serious refactoring.
5. Auto-generate без проверки
Уже обсуждали. Always review autogenerate output.
Тестирование миграций
# tests/test_migrations.py
import pytest
from alembic.config import Config
from alembic import command
def test_migrations_apply():
config = Config("alembic.ini")
config.set_main_option("sqlalchemy.url", "sqlite:///:memory:")
command.upgrade(config, "head")
# Если падает - что-то не так
def test_migrations_downgrade():
config = Config("alembic.ini")
config.set_main_option("sqlalchemy.url", "sqlite:///:memory:")
command.upgrade(config, "head")
command.downgrade(config, "base") # откат всех
command.upgrade(config, "head") # снова применить
Полезно для CI - убедиться что миграции и upgrade и downgrade работают.
Сравнение с Go и PHP
В Go популярен golang-migrate - простой CLI с plain SQL файлами для up/down миграций. Меньше abstraction чем Alembic, больше manual control. Работает с любой БД через одинаковый интерфейс.
В PHP стандарт - Phinx (часть Laminas/Symfony экосистемы) или встроенные миграции Symfony/Laravel. Концептуально похожи на Alembic: code-based миграции с up/down, autogenerate, version tracking.
В Python для SQLAlchemy - Alembic стандарт. Для Django - встроенные миграции (другой механизм с auto-detection).
Мини-задание
- Setup:
mkdir myproject && cd myproject
pip install sqlalchemy alembic
alembic init alembic
# Edit alembic.ini:
# sqlalchemy.url = sqlite:///app.db
# В env.py:
# from myapp.db import Base
# target_metadata = Base.metadata
- Create migration:
# myapp/db.py
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase): pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
alembic revision --autogenerate -m "initial schema"
alembic upgrade head
# Создаст users таблицу
- Изменение схемы:
# Добавляем поле email
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
email: Mapped[str] = mapped_column(unique=True) # новое поле
alembic revision --autogenerate -m "add email"
# Проверь сгенерированный файл!
alembic upgrade head
Что дальше
Освоили миграции с Alembic. В следующем уроке - аутентификация через JWT: реализация secure user auth для REST API.