Alembic для миграций базы данных с SQLAlchemy

16 октября 2026
Автор

Антон Ларичев

Что такое Alembic и зачем он нужен

При разработке реальных приложений схема базы данных постоянно меняется: добавляются новые таблицы, столбцы, индексы, меняются типы данных. Управлять этими изменениями вручную через SQL-скрипты неудобно и опасно — легко ошибиться, потерять историю изменений или рассинхронизировать окружения разработки и продакшена.

Alembic решает эту проблему: это инструмент миграций для SQLAlchemy, разработанный тем же автором (Michael Bayer). Он позволяет:

  • описывать изменения схемы как последовательность версионированных миграций
  • применять и откатывать миграции одной командой
  • автоматически генерировать миграции на основе изменений в моделях SQLAlchemy
  • отслеживать текущее состояние схемы в любом окружении

Курс по теме

Научиться программировать на Python

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Установка и первоначальная настройка

Установите Alembic вместе с SQLAlchemy и драйвером базы данных:

pip install alembic sqlalchemy psycopg2-binary

Для SQLite драйвер не нужен — он встроен в Python. Для MySQL используйте pymysql или mysqlclient.

Инициализируйте Alembic в корне вашего проекта:

alembic init alembic

Эта команда создаёт следующую структуру:

project/
├── alembic/
│   ├── versions/       # папка с файлами миграций
│   ├── env.py          # конфигурационный файл окружения
│   ├── README
│   └── script.py.mako  # шаблон для новых миграций
└── alembic.ini         # основной конфигурационный файл

Настройка подключения к базе данных

Откройте alembic.ini и укажите строку подключения:

# alembic.ini
sqlalchemy.url = postgresql://user:password@localhost/mydb

Однако хранить credentials в файле конфигурации — плохая практика. Лучше читать их из переменных окружения. Для этого отредактируйте alembic/env.py:

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

config = context.config

if config.config_file_name is not None:
    fileConfig(config.config_file_name)

# Переопределяем URL из переменной окружения
database_url = os.getenv("DATABASE_URL", "sqlite:///./app.db")
config.set_main_option("sqlalchemy.url", database_url)

target_metadata = None

def run_migrations_offline() -> None:
    url = config.get_main_option("sqlalchemy.url")
    context.configure(
        url=url,
        target_metadata=target_metadata,
        literal_binds=True,
        dialect_opts={"paramstyle": "named"},
    )
    with context.begin_transaction():
        context.run_migrations()

def run_migrations_online() -> None:
    connectable = engine_from_config(
        config.get_section(config.config_ini_section, {}),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )
    with connectable.connect() as connection:
        context.configure(
            connection=connection,
            target_metadata=target_metadata,
        )
        with context.begin_transaction():
            context.run_migrations()

if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()

Создание первой миграции вручную

Создайте новую миграцию командой:

alembic revision -m "create_users_table"

Alembic создаст файл в папке versions/ с уникальным идентификатором:

# alembic/versions/a1b2c3d4e5f6_create_users_table.py
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa

revision: str = 'a1b2c3d4e5f6'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None

def upgrade() -> None:
    op.create_table(
        'users',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('email', sa.String(length=255), nullable=False),
        sa.Column('username', sa.String(length=100), nullable=False),
        sa.Column('hashed_password', sa.String(length=255), nullable=False),
        sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'),
        sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
        sa.PrimaryKeyConstraint('id'),
        sa.UniqueConstraint('email'),
        sa.UniqueConstraint('username'),
    )
    op.create_index('ix_users_email', 'users', ['email'])

def downgrade() -> None:
    op.drop_index('ix_users_email', table_name='users')
    op.drop_table('users')

Каждая миграция содержит две функции:

  • upgrade() — применяет изменения
  • downgrade() — откатывает их

Автогенерация миграций из моделей SQLAlchemy

Главное преимущество Alembic — умение сравнивать текущее состояние базы данных с описанными моделями и автоматически генерировать миграции.

Сначала опишите модели с использованием декларативного стиля SQLAlchemy:

# app/models.py
from datetime import datetime
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
    username: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
    hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
    is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)

    posts: Mapped[list["Post"]] = relationship("Post", back_populates="author")

class Post(Base):
    __tablename__ = "posts"

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    title: Mapped[str] = mapped_column(String(200), nullable=False)
    body: Mapped[str] = mapped_column(Text, nullable=False)
    author_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
    published: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)

    author: Mapped["User"] = relationship("User", back_populates="posts")

Теперь подключите метаданные моделей в env.py:

# alembic/env.py
from app.models import Base

target_metadata = Base.metadata

Запустите автогенерацию:

alembic revision --autogenerate -m "add_posts_table"

Alembic сравнит модели с текущей схемой базы данных и сгенерирует необходимые команды в upgrade() и downgrade().

Важно: всегда просматривайте сгенерированные миграции перед применением. Autogenerate не распознаёт ряд изменений: переименование столбцов (воспринимает как удаление + добавление), изменения на уровне хранимых процедур и триггеров.

Применение и откат миграций

Посмотрите текущее состояние:

alembic current

Посмотрите историю миграций:

alembic history --verbose

Примените все ожидающие миграции:

alembic upgrade head

Примените конкретную миграцию по идентификатору:

alembic upgrade a1b2c3d4e5f6

Примените следующую миграцию (на один шаг вперёд):

alembic upgrade +1

Откатите последнюю миграцию:

alembic downgrade -1

Откатите до конкретной версии:

alembic downgrade a1b2c3d4e5f6

Откатите все миграции (до начального состояния):

alembic downgrade base

Продвинутые операции с op

Модуль op предоставляет широкий набор операций для изменения схемы:

from alembic import op
import sqlalchemy as sa

def upgrade() -> None:
    # Добавить столбец
    op.add_column('users',
        sa.Column('bio', sa.Text(), nullable=True)
    )

    # Изменить тип столбца
    op.alter_column('users', 'username',
        existing_type=sa.String(100),
        type_=sa.String(150),
        nullable=False
    )

    # Переименовать столбец
    op.alter_column('users', 'hashed_password',
        new_column_name='password_hash'
    )

    # Создать индекс
    op.create_index('ix_posts_author_id', 'posts', ['author_id'])

    # Создать уникальное ограничение
    op.create_unique_constraint('uq_posts_title_author', 'posts', ['title', 'author_id'])

    # Удалить ограничение
    op.drop_constraint('uq_posts_title_author', 'posts', type_='unique')

def downgrade() -> None:
    op.drop_column('users', 'bio')
    op.alter_column('users', 'username',
        existing_type=sa.String(150),
        type_=sa.String(100),
        nullable=False
    )

Миграции с переносом данных

Иногда вместе с изменением схемы нужно перенести существующие данные. Alembic позволяет выполнять SQL прямо в миграции:

from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import table, column

def upgrade() -> None:
    # Добавляем новый столбец
    op.add_column('users',
        sa.Column('display_name', sa.String(200), nullable=True)
    )

    # Переносим данные: заполняем display_name из username
    users = table('users',
        column('id', sa.Integer),
        column('username', sa.String),
        column('display_name', sa.String),
    )
    op.execute(
        users.update().values(display_name=users.c.username)
    )

    # Делаем столбец обязательным после заполнения
    op.alter_column('users', 'display_name', nullable=False)

def downgrade() -> None:
    op.drop_column('users', 'display_name')

Для сложных операций с данными удобнее использовать прямой SQL:

def upgrade() -> None:
    op.execute("""
        INSERT INTO user_roles (user_id, role)
        SELECT id, 'user' FROM users
        WHERE id NOT IN (SELECT user_id FROM user_roles)
    """)

Интеграция с приложением

В реальных проектах миграции часто запускают программно при старте приложения. Вот пример для FastAPI:

# app/database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from alembic.config import Config
from alembic import command
import os

DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./app.db")

engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def run_migrations() -> None:
    alembic_cfg = Config("alembic.ini")
    alembic_cfg.set_main_option("sqlalchemy.url", DATABASE_URL)
    command.upgrade(alembic_cfg, "head")

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
# app/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.database import run_migrations

@asynccontextmanager
async def lifespan(app: FastAPI):
    run_migrations()
    yield

app = FastAPI(lifespan=lifespan)

Типичные проблемы и решения

Конфликты веток в версиях миграций. При командной разработке могут возникнуть миграции с одинаковым down_revision. Alembic это обнаружит и сообщит об ошибке. Решение — создать миграцию-слияние:

alembic merge -m "merge_branches" a1b2c3 d4e5f6

Таблица alembic_version уже существует. Если база данных уже содержит таблицы, но Alembic запускается впервые, пометьте текущее состояние как базовую версию без применения изменений:

alembic stamp head

Autogenerate не видит изменений. Убедитесь, что target_metadata в env.py указывает на правильный объект Base.metadata, и что все модели импортированы до его использования:

# env.py — импортируем все модули с моделями явно
from app.models import Base  # noqa: F401 — этот импорт нужен для регистрации всех моделей
from app import users, posts, comments  # noqa: F401

target_metadata = Base.metadata

Проверка миграции без применения. Чтобы увидеть SQL без выполнения, используйте флаг --sql:

alembic upgrade head --sql

Организация миграций в большом проекте

Для крупных проектов рекомендуется держать модели и конфигурацию Alembic вместе с приложением:

project/
├── app/
│   ├── models/
│   │   ├── __init__.py   # экспортирует Base и все модели
│   │   ├── user.py
│   │   └── post.py
│   └── database.py
├── migrations/           # можно переименовать из alembic/
│   ├── versions/
│   └── env.py
└── alembic.ini

В alembic.ini измените путь к папке с миграциями:

script_location = migrations

Такой подход упрощает навигацию и делает структуру проекта более явной.

Заключение

Alembic — стандартный инструмент для управления миграциями в экосистеме SQLAlchemy. Он решает задачу версионирования схемы базы данных надёжно и гибко: от простых изменений структуры таблиц до сложных миграций данных. Начните с ручного написания миграций, чтобы понять их структуру, затем подключите автогенерацию для ускорения работы — и всегда проверяйте сгенерированный код перед применением.

Чтобы глубже изучить Python, работу с базами данных и построение полноценных бэкенд-приложений, рекомендуем курс на PurpleSchool:

Python-разработчик — курс на PurpleSchool

Стрелочка влевоPython argparse: разбор аргументов командной строки

Постройте личный план изучения Python до уровня Middle — бесплатно!

Python — часть карты развития Backend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Все гайды по Python

Как отправлять запросы с помощью requests в PythonПочему Python выводит значение без команды printКак работает команда print в PythonВозможности Python для автоматизации задачРабота с JSON в Python на примерахPython get — методы получения данныхКак находить и исправлять ошибки в PythonРабота с данными через API и внешние сервисыСтруктура и оформление кода PythonОсновы Django с PythonПолезные приёмы в Python для повседневной работыИспользование locals в Python для отладкиИнтеграция PHP и PythonКак выполнять HTTPS-запросы в PythonFastAPI Python — быстрый старт: создание REST API с нуляКак работать с API в Python
Ввод целого числа в PythonВедение логов в PythonУдаление данных в Python с помощью removeОбработка исключений с помощью try/except в PythonФункция super() в Python — как вызвать метод родителяСоздание собственных контекстных менеджеров в PythonРабота с символами программирования PythonРабота с переменной X в PythonКак скачать Python на компьютерРабота с классами в PythonПростая программа на Python для начинающихОсновы Python для тех, кто начинаетЧто нового в Python 3Python 1 — с чего начиналась история языкаКоманда python print - полное руководство по выводу данныхПоддерживается ли Python 2 и стоит ли его использоватьПравила именования переменных в PythonПользовательские исключения в PythonОсновы Python coreОписание объектов PythonНаследование классов в Python — основы и примерыМножественное наследование в Python — примеры и MROКонтекстный менеджер with в Python — как работает и зачем нуженКомментарии в Python — однострочные, многострочные и docstringКакой Python выбрать для установкиКак вывести целое число с помощью print в PythonКак установить Python на Windows macOS и LinuxКак пользоваться консолью PythonКак получить последний элемент в PythonКак найти значение в PythonКак настроить PythonКак использовать print для строк в PythonКак работает интерпретатор PythonИнструкция по работе с PythonБлок finally в обработке исключений PythonЦелые числа в PythonАбстрактные классы в Python — ABC и abstractmethod
uv — современный менеджер пакетов PythonPython subprocess: запуск внешних команд и процессовВеб-скрапинг с Scrapy в PythonSQLModel ORM для FastAPIPython rich: красивый вывод в терминалеPydantic: валидация данных в PythonPoetry: управление зависимостями в PythonPython pathlib — современная работа с файловыми путямиPython click: создание CLI-приложенийPython itertools — работа с итераторамиПарсинг сайтов с BeautifulSoup в PythonPython argparse: разбор аргументов командной строкиAlembic для миграций базы данных с SQLAlchemy
Загрузка данных PythonСоздание веб-приложений на Flask PythonСоздание бота на PythonСоздание интерфейсов Python QTУправление проектами на GitHub с PythonСоздание игр с PygameСоздание GUI в PythonКак работать со словарями в PythonРабота с библиотеками через Python PackagingРабота со временем в Python при помощи модуля timePython name — особенности переменнойМатематические операции в Python с модулем mathPython listing — что это и как использоватьОбработка изображений с OpenCV PythonPytest — тестирование на Python: полное руководствоNumPy в Python — основы и применение в задачахИспользование Anaconda с PythonМашинное обучение с PythonМодуль contextlib в Python — утилиты для контекстных менеджеровБиблиотеки Python и их применение в проектах
Возврат значений из функции в PythonВложенные функции в PythonСоздание собственных декораторов в PythonРабота с функцией map в PythonЦикл while в Python и примеры использованияОбработка чисел, введённых через input в PythonОсновные операторы в Python с примерамиУсловные выражения if else в Python для начинающихКак выполняется вызов функций call в PythonПродвинутые генераторы в Python — send, throw, close и корутиныПозиционные и именованные аргументы в PythonОбъявление переменных и управление областью видимости в PythonПередача аргументов по ссылке и по значению в PythonПередача аргументов через args и kwargs в PythonОператор match/case в Python 3.10+ — основы структурного сопоставленияОсновные методы Python и примеры их использованияПаттерны match/case в Python — деструктуризация, guard и вложенные шаблоныПрактические примеры match/case в Python — реальные сценарии примененияЛокальные и глобальные переменные в PythonЧасто используемые команды PythonКлючевые слова global и nonlocal в PythonКак создавать функции в PythonКак работает сборщик мусора в PythonКак работает область видимости переменных в PythonКак работает функция callable в PythonКак работает функция any и all в PythonКак проверить тип переменной в PythonКак передать функцию как аргумент в PythonКак использовать функцию isinstance в PythonКак использовать функцию filter в PythonКак использовать функцию filter в PythonКак использовать функцию eval безопасно в PythonКак использовать декораторы в PythonИзменяемые и неизменяемые типы данных в PythonГенераторы и yield в Python — как создавать и использоватьГенераторные выражения в Python — синтаксис и примерыФункции в Python и способы их вызоваФункции как объекты в PythonЧто такое замыкания в PythonЧто делает функция id в PythonЧто делает функция reduce в PythonАргументы по умолчанию в PythonАнонимные функции и lambda в PythonАлгоритмы на Python — примеры и объяснение
Запись данных в PythonУстановка pip в PythonУправление зависимостями requirement в PythonУправление библиотеками с помощью Python PackagingУдаление пробелов с помощью strip в PythonСоздание исполняемого файла Python в exeСтруктурирование кода в PythonРазбор traceback в модуле PythonРазбор site-packages в PythonРазбор Program Files в PythonРабота с Unicode кодировками в PythonРабота с системными функциями Python sysРабота с папкой AppData в PythonРабота с модулем logging в PythonРабота с каталогами в PythonРабота с CSV в PythonВиртуальная среда venv в Python — создание и настройкаКак создать простое приложение на PythonИспользование pip в Python для установки пакетовМодули в Python и организация кода в проектеИмпорт модулей в Python и правила подключенияРабота с файлами в Python пошаговоЧто делает компилятор Python и как он работаетПолучение строки из модуля PythonПодключение файлов в Python с includeПеременные среды в PythonСборка проекта с помощью packaging в PythonНастройка Python сервераИспользование Python на UbuntuИспользование консоли PythonИспользование кодировок в PythonИнициализация пакетов PythonИмпорт модулей PythonИмпорт имен в PythonСреда IDLE Python и базовые возможностиЧтение и запись TXT в PythonЧтение файлов в Python с помощью open file
Удаление элементов из списка PythonТипы данных в Python — обзор и рекомендацииОсновные операции со строками в PythonМетоды str в Python и обработка текстаСписки в Python и их ключевые методыСоздание списков данных в PythonРабота со строками и символами в PythonРабота со столбцами в PythonРабота со списком значений в PythonРабота с таблицами в Python с помощью DataFrameРабота с RFR в PythonРабота с пробелами в PythonРабота с массивами в PythonРабота с кортежами tuple PythonРабота с координатами X и Y в PythonРабота с ключами в PythonРабота с элементами данных PythonРабота с двоичными числами PythonРабота с данными в PythonРабота с данными NumPy PythonРабота с большими числами в PythonРабота с битами в PythonРабота с байтами в PythonЧто такое значение в Python и как его определитьМножества в Python и операции с нимиИспользование range в Python для цикловПроверка на четность в PythonПроверка числа в PythonПреобразование типов в PythonПреобразование списка в строку PythonПреобразование числа в строку в PythonПостроение графиков в PythonОпределение индекса элемента в PythonОкругление чисел в PythonОбъединение списков в Python с помощью zipМножества в PythonМассивы в Python и отличие от списковМассив чисел в PythonКортежи данных в PythonКак вычислить сумму чисел в PythonКак получить остаток от деления в PythonКак найти следующее число в PythonИспользование Unicode в PythonТип int в Python и его особенностиИндекс списка в PythonФункции для работы со строками в PythonЭлементы Python и способы доступа к нимДоступ к элементам массива в PythonДеление чисел в PythonРабота с данными в Python на практикеКак работать с числами в Python
Телеграм-бот на Python: от команды до деплояRedis в Python с asyncioPython multiprocessing: параллельные процессыMotor: асинхронная работа с MongoDB в PythonPython httpx: современный HTTP-клиентFastAPI WebSockets в реальном времениFastAPI middleware и обработка запросовFastAPI с JWT аутентификациейconcurrent.futures: параллельные задачи в PythonФоновые задачи с Celery в PythonАсинхронная работа с PostgreSQL в Python через asyncpgPython asyncio.TaskGroupМодуль asyncio в Python — основы асинхронного программированияСинтаксис async/await в Python — как писать асинхронный кодБиблиотека aiohttp в Python — асинхронные HTTP-запросы
Открыть базу знаний

Лучшие курсы по теме

Иконка молнииНовый
изображение курса

Основы Python

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий