← Назад к вопросам

Как реализовать миграцию в SQLAlchemy?

2.0 Middle🔥 81 комментариев
#Базы данных (SQL)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Миграции в SQLAlchemy

Миграции - это управляемые изменения структуры БД. Есть несколько подходов.

1. Основные инструменты

# Самый популярный инструмент для миграций с SQLAlchemy
pip install alembic

# Инициализация
alembic init migrations

# Структура
.
├── migrations/
│   ├── env.py               # Конфигурация
│   ├── script.py.mako      # Шаблон для новых миграций
│   └── versions/           # Основные файлы миграций
│       ├── 001_initial.py
│       ├── 002_add_users.py
│       └── 003_add_email.py
├── alembic.ini             # Основной конфиг
└── models.py               # SQLAlchemy модели

2. Простой пример: добавить таблицу

# models.py
from sqlalchemy import Column, Integer, String, DateTime, Boolean, create_engine
from sqlalchemy.orm import declarative_base
from datetime import datetime

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50), unique=True, nullable=False)
    email = Column(String(100), unique=True, nullable=False)
    is_active = Column(Boolean, default=True)
    created_at = Column(DateTime, default=datetime.utcnow)
# 1. Создать миграцию
alembic revision --autogenerate -m "Create users table"

# 2. Проверить сгенерированный файл migrations/versions/001_create_users.py

# 3. Применить миграцию
alembic upgrade head

# 4. Откатить миграцию (если нужно)
alembic downgrade -1

3. Автоматическая генерация миграций

# alembic/env.py
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
import os
from dotenv import load_dotenv

from app.models import Base  # Импортируем наши модели!

load_dotenv()

# Конфиг из alembic.ini
config = context.config

# Подставляем DATABASE_URL из .env
db_url = os.getenv('DATABASE_URL')
config.set_main_option('sqlalchemy.url', db_url)

# Это КРИТИЧНО для autogenerate!
target_metadata = Base.metadata

def run_migrations_online():
    """Запустить миграции"""
    engine = engine_from_config(
        config.get_section(config.config_ini_section),
        prefix='sqlalchemy.',
        poolclass=pool.NullPool,
    )
    
    with engine.connect() as connection:
        context.configure(
            connection=connection,
            target_metadata=target_metadata  # Для autogenerate!
        )
        
        with context.begin_transaction():
            context.run_migrations()

if context.is_offline_mode():
    # Offline mode для генерации SQL
    context.configure(target_metadata=target_metadata)
    with context.begin_transaction():
        context.run_migrations()
else:
    run_migrations_online()

4. Ручное написание миграций

Иногда autogenerate не всё видит, нужно написать вручную:

# migrations/versions/002_add_phone_to_users.py
from alembic import op
import sqlalchemy as sa

revision = '002_add_phone_to_users'
down_revision = '001_create_users'
branch_labels = None
depends_on = None

def upgrade():
    """Применить изменение"""
    # Добавить колонку
    op.add_column('users', sa.Column('phone', sa.String(20), nullable=True))
    
    # Создать индекс
    op.create_index('ix_users_phone', 'users', ['phone'])

def downgrade():
    """Откатить изменение"""
    op.drop_index('ix_users_phone', 'users')
    op.drop_column('users', 'phone')

5. Сложные миграции

# Сложная миграция с данными
from alembic import op
import sqlalchemy as sa
from sqlalchemy.orm import Session

revision = '003_migrate_data'
down_revision = '002_add_phone_to_users'

def upgrade():
    """Сложная миграция с обработкой данных"""
    
    # Создать новую таблицу
    op.create_table('user_profiles',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('user_id', sa.Integer(), nullable=False),
        sa.Column('bio', sa.String(500), nullable=True),
        sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
        sa.PrimaryKeyConstraint('id')
    )
    
    # Выполнить raw SQL запрос
    op.execute("""
        INSERT INTO user_profiles (user_id, bio)
        SELECT id, 'Auto-created' FROM users
    """)
    
    # Удалить старую колонку
    op.drop_column('users', 'bio')

def downgrade():
    """Откатить сложную миграцию"""
    op.create_table('users',
        sa.Column('bio', sa.String(500), nullable=True),
        # ...
    )
    op.drop_table('user_profiles')

6. Миграция с foreign keys и constraints

# migrations/versions/004_create_posts.py
from alembic import op
import sqlalchemy as sa

def upgrade():
    """Создать таблицу posts с FK на users"""
    op.create_table('posts',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('user_id', sa.Integer(), nullable=False),
        sa.Column('title', sa.String(200), nullable=False),
        sa.Column('content', sa.Text(), nullable=False),
        sa.Column('created_at', sa.DateTime(), nullable=False),
        sa.ForeignKeyConstraint(
            ['user_id'],
            ['users.id'],
            name='fk_posts_user_id',
            ondelete='CASCADE'  # Важно для data integrity!
        ),
        sa.PrimaryKeyConstraint('id')
    )
    
    # Индексы для производительности
    op.create_index('ix_posts_user_id', 'posts', ['user_id'])
    op.create_index('ix_posts_created_at', 'posts', ['created_at'])

def downgrade():
    op.drop_index('ix_posts_created_at', table_name='posts')
    op.drop_index('ix_posts_user_id', table_name='posts')
    op.drop_table('posts')

7. Branching и объединение миграций

# Когда несколько разработчиков пишут миграции
# Может быть конфликт версий

# Миграция 1 от developer A
alembic revision -m "Add column A"
# Creates: 003_add_column_a.py

# Миграция 2 от developer B (параллельно)
alembic revision -m "Add column B"
# Creates: 003_add_column_b.py (конфликт!)

# Решение: использовать branch points
alembic revision --branch-label feature_a -m "Add column A"
alembic revision --branch-label feature_b -m "Add column B"
alembic merge --branch-label integration -m "Merge branches"

8. История миграций

# Посмотреть текущую версию БД
alembic current

# Посмотреть все миграции
alembic history

# Посмотреть какие миграции не применены
alembic heads

# Поднять до определенной версии
alembic upgrade 002_add_phone_to_users

# Опустить на одну версию
alembic downgrade -1

# Применить все до последней
alembic upgrade head

9. Миграции в тестах

# tests/conftest.py
import pytest
from alembic.config import Config
from alembic import command
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

@pytest.fixture(scope='session')
def engine():
    """Создать тестовую БД"""
    engine = create_engine('sqlite:///:memory:')
    return engine

@pytest.fixture(scope='session')
def apply_migrations(engine):
    """Применить миграции перед тестами"""
    config = Config('alembic.ini')
    config.set_main_option('sqlalchemy.url', str(engine.url))
    command.upgrade(config, 'head')
    return True

@pytest.fixture
def db_session(engine, apply_migrations):
    """Сессия для каждого теста"""
    connection = engine.connect()
    transaction = connection.begin()
    Session = sessionmaker(bind=connection)
    session = Session()
    
    yield session
    
    session.close()
    transaction.rollback()
    connection.close()

# Использование в тестах
def test_create_user(db_session):
    from app.models import User
    
    user = User(username='alice', email='alice@example.com')
    db_session.add(user)
    db_session.commit()
    
    assert db_session.query(User).filter_by(username='alice').first()

10. Best practices

# ✅ ХОРОШО: descriptive названия
alembic revision --autogenerate -m "Add email verification to users"
# ✓ Creates: 001_add_email_verification_to_users.py

# ❌ ПЛОХО: неясные названия
alembic revision --autogenerate -m "Update"
# ✗ Creates: 001_update.py

# ✅ ХОРОШО: обратимые миграции
def upgrade():
    op.add_column('users', sa.Column('is_verified', sa.Boolean(), default=False))

def downgrade():
    op.drop_column('users', 'is_verified')

# ✅ ХОРОШО: индексы для производительности
def upgrade():
    op.add_column('users', sa.Column('email', sa.String(100)))
    op.create_index('ix_users_email', 'users', ['email'])

# ✅ ХОРОШО: миграции с данными (если необходимо)
def upgrade():
    op.execute("UPDATE users SET is_active = true WHERE is_active IS NULL")

# ✅ ХОРОШО: используй context для SQL агностики
from alembic.operations import Operations
from sqlalchemy.sql import text

def upgrade():
    op.execute(text("UPDATE users SET status = 'active'"))

11. Альтернатива: Goose (для raw SQL миграций)

# Если предпочитаешь писать raw SQL вместо SQLAlchemy
pip install goose

# Инициализация
goose init migrations

# Создать миграцию
goose create create_users sql

# Structure:
# migrations/
# ├── 001_create_users.sql
# └── 002_add_posts.sql
-- migrations/001_create_users.up.sql
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX ix_users_username ON users(username);

-- migrations/001_create_users.down.sql
DROP TABLE users;
# Применить миграции
goose up

# Откатить на одну версию
goose down

# Посмотреть статус
goose status

12. Полный пример: от моделей к БД

# 1. Создать проект
mkdir myapp && cd myapp

# 2. Установить зависимости
pip install sqlalchemy alembic python-dotenv

# 3. Инициализировать Alembic
alembic init migrations

# 4. Создать models.py
cat > models.py << 'EOF'
from sqlalchemy import Column, Integer, String, Email, DateTime, create_engine
from sqlalchemy.orm import declarative_base
from datetime import datetime

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    username = Column(String(50), unique=True, nullable=False)
    email = Column(String(100), unique=True, nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow)
EOF

# 5. Настроить alembic/env.py
# (добавить импорт Base и target_metadata = Base.metadata)

# 6. Создать миграцию
alembic revision --autogenerate -m "Create users table"

# 7. Применить миграцию
alembic upgrade head

# 8. Проверить что таблица создана
sqlite3 < 'sqlite:///app.db' ".tables"

Заключение

Миграции в SQLAlchemy:

  1. Alembic - стандартный инструмент
  2. Autogenerate - автоматическая генерация из моделей
  3. Версионирование - каждая миграция имеет номер
  4. Upgrade/Downgrade - легко откатывать изменения
  5. Reproducible - одинаковое состояние БД везде
  6. Version control - миграции лежат в Git

Это критично для production систем!

Как реализовать миграцию в SQLAlchemy? | PrepBro