← Назад к вопросам
Как реализовать миграцию в 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:
- Alembic - стандартный инструмент
- Autogenerate - автоматическая генерация из моделей
- Версионирование - каждая миграция имеет номер
- Upgrade/Downgrade - легко откатывать изменения
- Reproducible - одинаковое состояние БД везде
- Version control - миграции лежат в Git
Это критично для production систем!