SQLAlchemy ORM в Python

13 августа 2026
Автор

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

SQLAlchemy — самая популярная библиотека для работы с базами данных в Python. Её ORM-слой (Object-Relational Mapping) позволяет описывать таблицы как Python-классы и работать с записями как с обычными объектами, не пиша SQL вручную. В этой статье разберём SQLAlchemy ORM от подключения к базе данных до сложных запросов и связей.

Установка и подключение

Установите SQLAlchemy и драйвер для нужной СУБД:

pip install sqlalchemy
# Для PostgreSQL:
pip install psycopg2-binary
# Для MySQL:
pip install pymysql
# SQLite встроен в Python — дополнительный драйвер не нужен

Подключение к базе данных создаётся через create_engine. Строка подключения определяет СУБД, учётные данные и имя базы:

from sqlalchemy import create_engine

# SQLite (файл на диске)
engine = create_engine("sqlite:///app.db", echo=True)

# PostgreSQL
engine = create_engine(
    "postgresql+psycopg2://user:password@localhost:5432/mydb"
)

# MySQL
engine = create_engine(
    "mysql+pymysql://user:password@localhost:3306/mydb"
)

Параметр echo=True выводит генерируемый SQL в консоль — удобно при разработке.

Курс по теме

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

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

Декларативный стиль: создание моделей

Современный способ описания моделей в SQLAlchemy 2.x — декларативный стиль с аннотациями типов. Каждый класс модели наследуется от DeclarativeBase:

from sqlalchemy import String, Integer, Text, DateTime
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from datetime import datetime

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True)
    username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
    email: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
    bio: Mapped[str | None] = mapped_column(Text)
    created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)

    def __repr__(self) -> str:
        return f"<User id={self.id} username={self.username!r}>"

Ключевые элементы модели:

  • __tablename__ — имя таблицы в базе данных
  • Mapped[тип] — аннотация с типом столбца; если тип обёрнут в Optional или тип | None, столбец допускает NULL
  • mapped_column() — настройки столбца: первичный ключ, уникальность, дефолтное значение

Создание таблиц

Метод create_all создаёт все таблицы, зарегистрированные в Base.metadata:

Base.metadata.create_all(engine)

Для удаления всех таблиц используется drop_all — в продакшене делайте это только намеренно.

Сессия: точка входа в ORM

Все операции с базой данных проходят через Session. Сессия отслеживает изменения объектов и фиксирует их транзакцией при вызове commit.

from sqlalchemy.orm import Session

# Создание сессии вручную
session = Session(engine)

# Либо через фабрику (рекомендуется)
from sqlalchemy.orm import sessionmaker

SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
session = SessionLocal()

В реальных приложениях сессию открывают как контекстный менеджер, чтобы она автоматически закрывалась:

with Session(engine) as session:
    # все операции внутри блока
    session.commit()

CRUD-операции

Создание записей (Create)

with Session(engine) as session:
    user = User(
        username="ivan_petrov",
        email="ivan@example.com",
        bio="Backend developer"
    )
    session.add(user)
    session.commit()
    session.refresh(user)  # обновляет объект данными из БД (id, created_at)
    print(user.id)  # 1

Для массовой вставки используйте add_all:

with Session(engine) as session:
    users = [
        User(username="alice", email="alice@example.com"),
        User(username="bob", email="bob@example.com"),
        User(username="carol", email="carol@example.com"),
    ]
    session.add_all(users)
    session.commit()

Чтение записей (Read)

Для запросов используется метод session.execute() с объектами select:

from sqlalchemy import select

with Session(engine) as session:
    # Получить по первичному ключу
    user = session.get(User, 1)
    print(user)  # <User id=1 username='ivan_petrov'>

    # Получить одну запись по условию
    stmt = select(User).where(User.username == "alice")
    user = session.execute(stmt).scalar_one_or_none()

    # Получить все записи
    stmt = select(User).order_by(User.created_at.desc())
    users = session.execute(stmt).scalars().all()
    for u in users:
        print(u.username)

Методы получения результатов:

  • scalar_one() — ровно одна запись, иначе исключение
  • scalar_one_or_none() — одна запись или None
  • scalars().all() — список всех записей
  • scalars().first() — первая запись или None

Обновление записей (Update)

with Session(engine) as session:
    user = session.get(User, 1)
    if user:
        user.bio = "Senior Backend Developer"
        session.commit()

Сессия автоматически отслеживает изменения атрибутов — достаточно изменить поле и вызвать commit. Для массового обновления применяйте update:

from sqlalchemy import update

with Session(engine) as session:
    stmt = (
        update(User)
        .where(User.email.like("%@example.com"))
        .values(bio="Example company employee")
    )
    session.execute(stmt)
    session.commit()

Удаление записей (Delete)

with Session(engine) as session:
    user = session.get(User, 1)
    if user:
        session.delete(user)
        session.commit()

Массовое удаление:

from sqlalchemy import delete

with Session(engine) as session:
    stmt = delete(User).where(User.username == "bob")
    session.execute(stmt)
    session.commit()

Связи между моделями

Одно из главных преимуществ ORM — декларативное описание связей между таблицами.

Один ко многим (One-to-Many)

from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship

class Post(Base):
    __tablename__ = "posts"

    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(200), nullable=False)
    body: Mapped[str] = mapped_column(Text)
    user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False)

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

# Добавляем обратную связь в User
class User(Base):
    __tablename__ = "users"
    # ... остальные поля ...
    posts: Mapped[list["Post"]] = relationship(back_populates="author", cascade="all, delete-orphan")

Параметр cascade="all, delete-orphan" означает, что при удалении пользователя все его посты тоже удаляются.

Пример использования:

with Session(engine) as session:
    user = session.get(User, 2)
    post = Post(title="Первый пост", body="Содержимое поста", author=user)
    session.add(post)
    session.commit()

    # Обращение к связанным объектам
    for post in user.posts:
        print(post.title)

Многие ко многим (Many-to-Many)

Для связи «многие ко многим» нужна промежуточная таблица:

from sqlalchemy import Table, Column

# Ассоциативная таблица
post_tags = Table(
    "post_tags",
    Base.metadata,
    Column("post_id", ForeignKey("posts.id"), primary_key=True),
    Column("tag_id", ForeignKey("tags.id"), primary_key=True),
)

class Tag(Base):
    __tablename__ = "tags"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(50), unique=True)
    posts: Mapped[list["Post"]] = relationship(
        secondary=post_tags, back_populates="tags"
    )

# В модели Post добавляем:
# tags: Mapped[list[Tag]] = relationship(secondary=post_tags, back_populates="posts")

Фильтрация и сложные запросы

Фильтры и операторы

from sqlalchemy import select, or_, and_, func

with Session(engine) as session:
    # Несколько условий AND
    stmt = select(User).where(
        and_(User.username.startswith("a"), User.bio.isnot(None))
    )

    # Условие OR
    stmt = select(User).where(
        or_(User.email.like("%@gmail.com"), User.email.like("%@yandex.ru"))
    )

    # Сортировка и лимит
    stmt = (
        select(User)
        .order_by(User.created_at.desc())
        .limit(10)
        .offset(20)
    )

    # Подсчёт записей
    stmt = select(func.count()).select_from(User)
    count = session.execute(stmt).scalar()
    print(f"Всего пользователей: {count}")

JOIN-запросы

with Session(engine) as session:
    # Получить посты с информацией об авторе
    stmt = (
        select(Post, User)
        .join(User, Post.user_id == User.id)
        .where(User.username == "alice")
    )
    results = session.execute(stmt).all()
    for post, user in results:
        print(f"{user.username}: {post.title}")

Жадная загрузка связанных объектов

По умолчанию SQLAlchemy загружает связанные объекты «лениво» — отдельным запросом при первом обращении. Это приводит к проблеме N+1 запросов. Решение — жадная загрузка через selectinload или joinedload:

from sqlalchemy.orm import selectinload, joinedload

with Session(engine) as session:
    # selectinload — отдельный SELECT для связанных объектов (рекомендуется для коллекций)
    stmt = select(User).options(selectinload(User.posts))
    users = session.execute(stmt).scalars().all()
    for user in users:
        print(f"{user.username}: {len(user.posts)} постов")

    # joinedload — JOIN в одном запросе (рекомендуется для единичных связей)
    stmt = select(Post).options(joinedload(Post.author))
    posts = session.execute(stmt).scalars().all()
    for post in posts:
        print(f"{post.title} by {post.author.username}")

Валидация и события

SQLAlchemy поддерживает хуки через декоратор @event.listens_for и валидаторы через @validates:

from sqlalchemy.orm import validates
from sqlalchemy import event

class User(Base):
    __tablename__ = "users"
    # ... поля ...

    @validates("email")
    def validate_email(self, key, email):
        if "@" not in email:
            raise ValueError(f"Некорректный email: {email}")
        return email.lower()

@event.listens_for(User, "before_insert")
def set_default_bio(mapper, connection, target):
    if target.bio is None:
        target.bio = "Описание не указано"

Управление схемой: Alembic

Для управления миграциями базы данных вместе с SQLAlchemy используется Alembic:

pip install alembic
alembic init migrations

Alembic отслеживает изменения моделей и генерирует миграции автоматически:

# Создать миграцию на основе изменений в моделях
alembic revision --autogenerate -m "add posts table"

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

# Откатить последнюю миграцию
alembic downgrade -1

Миграции позволяют безопасно изменять схему базы данных в продакшене, не теряя данные.

Паттерн Repository

В приложениях с бизнес-логикой принято инкапсулировать запросы в репозиториях:

class UserRepository:
    def __init__(self, session: Session):
        self.session = session

    def get_by_id(self, user_id: int) -> User | None:
        return self.session.get(User, user_id)

    def get_by_email(self, email: str) -> User | None:
        stmt = select(User).where(User.email == email)
        return self.session.execute(stmt).scalar_one_or_none()

    def create(self, username: str, email: str) -> User:
        user = User(username=username, email=email)
        self.session.add(user)
        self.session.flush()  # Сохраняет в БД без commit, получает id
        return user

    def list_active(self, limit: int = 100) -> list[User]:
        stmt = select(User).order_by(User.created_at.desc()).limit(limit)
        return self.session.execute(stmt).scalars().all()


# Использование
with Session(engine) as session:
    repo = UserRepository(session)
    user = repo.create("diana", "diana@example.com")
    session.commit()
    print(user.id)

Паттерн Repository делает код тестируемым: в тестах можно подменить реальную сессию на мок или использовать SQLite in-memory.

Итог

SQLAlchemy ORM предоставляет мощный инструментарий для работы с реляционными базами данных в Python:

  • Модели описывают таблицы как Python-классы с типизированными полями
  • Сессия управляет транзакциями и отслеживает изменения объектов
  • Связи (relationship) позволяют работать со связанными данными как с атрибутами объектов
  • select с фильтрами, JOIN и жадной загрузкой закрывает большинство сценариев выборки
  • Alembic берёт на себя управление миграциями схемы

Изучить Python и SQLAlchemy на практике с разбором реальных проектов можно на курсе Python на PurpleSchool.

Python __slots__ — оптимизация памяти в классахСтрелочка вправо

Постройте личный план изучения 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 3Поддерживается ли Python 2 и стоит ли его использоватьPython 1 — с чего начиналась история языкаКоманда python print - полное руководство по выводу данныхПравила именования переменных в 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
Загрузка данных PythonУправление проектами на GitHub с PythonСоздание веб-приложений на Flask PythonСоздание бота на PythonСоздание интерфейсов Python QTСоздание игр с PygameСоздание GUI в PythonКак работать со словарями в PythonРабота с библиотеками через Python PackagingРабота со временем в Python при помощи модуля timePython name — особенности переменнойМатематические операции в Python с модулем mathPython listing — что это и как использоватьPytest — тестирование на Python: полное руководствоОбработка изображений с OpenCV PythonNumPy в Python — основы и применение в задачахМашинное обучение с PythonИспользование Anaconda с 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Основные методы Python и примеры их использованияОператор match/case в Python 3.10+ — основы структурного сопоставленияПаттерны 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Что делает функция reduce в PythonЧто делает функция id в PythonАргументы по умолчанию в PythonАнонимные функции и lambda в PythonАлгоритмы на Python — примеры и объяснение
Запись данных в PythonУстановка pip в PythonУправление зависимостями requirement в PythonУправление библиотеками с помощью Python PackagingУдаление пробелов с помощью strip в PythonСтруктурирование кода в PythonСоздание исполняемого файла Python в exeРазбор 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

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

Nest.js с нуля

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

Docker и Ansible

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

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