Какая mental model SQLAlchemy важнее всего: unit of work, change tracking, query generation, migrations или generated client?
Сначала попробуйте ответить сами30 секунд
Центральная mental model — Unit of Work: сессия накапливает изменения и сбрасывает их единым батчем. Всё остальное (change tracking, query generation, migrations) — следствие или надстройка над этой концепцией.
Что такое Unit of Work
Unit of Work — это паттерн (описан Мартином Фаулером), при котором объект-сессия отслеживает все изменения, внесённые за время транзакции, и сбрасывает их в БД единым согласованным батчем в момент коммита или явного flush(). SQLAlchemy Session является классической реализацией этого паттерна.
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session
from models import User, Address
engine = create_engine("postgresql+psycopg2://user:pw@localhost/db")
with Session(engine) as session:
# 1. Загружаем объект — он теперь "tracked"
user = session.get(User, 42)
# 2. Меняем атрибут — изменение запоминается в identity map
user.name = "Alice"
# 3. Добавляем связанный объект
addr = Address(city="Berlin", user=user)
session.add(addr)
# 4. flush() превращает накопленные изменения в SQL
# (происходит автоматически перед коммитом)
session.flush()
# INSERT INTO addresses ... + UPDATE users SET name=... выполняются здесь
session.commit()
# транзакция фиксируется; identity map сбрасывается (expire_on_commit=True)
Unit of Work — центральная mental model SQLAlchemy
Из всех концепций SQLAlchemy самой важной для практической работы является Unit of Work. Остальные — change tracking, query generation, migrations, generated client — либо производные от неё, либо вспомогательные инструменты. Понимание Unit of Work объясняет, почему ORM ведёт себя именно так, как ведёт, и предотвращает большую часть типичных ошибок.
Change Tracking как следствие Unit of Work
SQLAlchemy отслеживает изменения атрибутов через инструментирование (instrumented attributes). Каждый столбец модели заменяется дескриптором, который при записи помечает объект как «грязный» (dirty) в сессии. Это позволяет автоматически генерировать UPDATE только для изменившихся столбцов без явного указания.
from sqlalchemy import inspect
with Session(engine) as session:
user = session.get(User, 42)
user.email = "new@example.com"
insp = inspect(user)
print(insp.attrs.email.history) # History(added=['new@example.com'], unchanged=(), deleted=['old@example.com'])
print(session.dirty) # {<User id=42>}
Query Generation — инструмент, а не суть
ORM-запросы через select(User).where(User.age > 30) удобны, но это лишь DSL над SQL. Главное, что query generation работает внутри той же сессии и возвращает объекты, уже помещённые в identity map. Если вы загрузили объект через get() и потом делаете select() с тем же PK — вы получаете тот же Python-объект из памяти, а не новый из БД.
Migrations и Generated Client — отдельные слои
Alembic (migrations) работает независимо от сессии и Unit of Work — он сравнивает метаданные моделей с реальной схемой БД и генерирует DDL-скрипты. Generated client (например, через automap_base() или sqlc) — ещё один вспомогательный инструмент для случаев, когда схема первична.
Почему именно Unit of Work важнее всего
- Объясняет, зачем нужен
session.add()и что происходит приsession.commit(). - Помогает понять
expire_on_commit=True: после коммита все атрибуты помечаются устаревшими, следующее обращение — новый SELECT. - Объясняет
DetachedInstanceError: объект вне сессии не может лениво загружать атрибуты. - Объясняет, почему два
session.get(User, 42)подряд возвращают один объект (identity map). - Помогает правильно выбрать границы транзакции и избежать «фантомных» запросов.
Подводные камни
- expire_on_commit: после коммита каждое обращение к атрибуту вне открытой сессии вызывает отдельный SELECT. В async-коде это приводит к
MissingGreenlet/greenlet_spawnошибкам. - Autoflush: SQLAlchemy делает
flush()перед каждым запросом по умолчанию (autoflush=True). Это ломает ожидания, когда вы не готовы к записи в этот момент. - Identity map и stale data: объект в памяти не обновляется автоматически, если другой процесс изменил строку в БД.
session.refresh(obj)решает проблему. - Область жизни сессии: сессия не является потокобезопасной и не должна шариться между запросами. В FastAPI это решается через
async_scoped_sessionили dependency injection. - Cascade без осознания:
cascade="all, delete-orphan"удалит дочерние записи при удалении родителя — это не всегда желательное поведение. - Слишком долгая сессия: держать открытую сессию через несколько HTTP-запросов — антипаттерн; identity map разрастается и данные стареют.
- Detached objects: передача ORM-объектов за пределы сессии без eager load всех нужных атрибутов приводит к
DetachedInstanceErrorв продакшне. - bulk operations обходят Unit of Work:
session.execute(update(User).values(...))не обновит уже загруженные в identity map объекты.
Частые вопросы на собеседовании
Что портит ответ
- Знать термины SQLAlchemy, но не понимать связи между абстракциями
- Объяснять поведение через отдельные примеры вместо причинной модели
- Не связывать mental model с диагностикой ошибок
Что слушает интервьюер
- Понимает ключевые абстракции SQLAlchemy
- Может предсказывать поведение системы через mental model
- Связывает модель с debugging и production decisions