В чём разница между Session.add(), Session.merge() и Session.get()?
add() регистрирует новый объект (INSERT при flush), merge() переносит detached-объект в сессию с возможным SELECT+UPDATE, get() ищет по PK с кэшем identity map.
Session.add(), Session.merge() и Session.get() — три разных контракта
Эти три метода решают разные задачи в рамках Unit of Work — паттерна, лежащего в основе SQLAlchemy Session.
Session.add()
Помещает объект в сессию со статусом pending (новый) или переводит detached-объект обратно в persistent. Не делает INSERT немедленно — запрос уйдёт при flush() или commit().
from sqlalchemy.orm import Session
from models import User
with Session(engine) as session:
user = User(name="Alice", email="alice@example.com")
session.add(user) # статус: pending
session.commit() # flush + commit → INSERT выполнен
print(user.id) # id заполнен после flush
Если объект уже persistent (привязан к этой же сессии), повторный add() — нет-оп.
Session.merge()
Принимает объект (часто detached или вообще не привязанный к сессии) и возвращает managed копию. SQLAlchemy сначала ищет объект в identity map по первичному ключу, затем при необходимости делает SELECT, и наконец копирует атрибуты. Используется при работе с объектами, пришедшими извне (десериализация, другая сессия, кэш).
with Session(engine) as session:
# detached_user пришёл из другой сессии или был десериализован
managed_user = session.merge(detached_user)
managed_user.name = "Bob"
session.commit() # UPDATE выполнен для managed_user
Важно: merge() может генерировать SELECT перед UPDATE. Если нужна только upsert-семантика без лишних запросов, смотрите на insert().on_conflict_do_update() из SQLAlchemy Core.
Session.get()
Загружает объект по первичному ключу. Сначала проверяет identity map (кэш сессии) — если объект уже загружен, SELECT не делается. Возвращает None, если запись не найдена.
with Session(engine) as session:
user = session.get(User, 42) # SELECT, если нет в identity map
if user is None:
raise ValueError("User not found")
# Второй вызов — из кэша, без SQL:
same_user = session.get(User, 42) # нет SELECT
assert user is same_user # один и тот же объект
Для SQLAlchemy 2.0 можно передать опции загрузки:
from sqlalchemy.orm import joinedload
user = session.get(
User,
42,
options=[joinedload(User.orders)]
)
Сравнение
add()— регистрирует новый или возвращённый объект; INSERT при flush.merge()— копирует состояние detached-объекта в managed; может SELECT + UPDATE.get()— поиск по PK с кэшированием в identity map; SELECT только при промахе кэша.
Подводные камни
session.add(detached_obj)вместоmerge()броситDetachedInstanceErrorпри обращении к lazy-атрибутам после повторного attach.merge()делает SELECT по PK даже если вы знаете, что запись существует — это лишний roundtrip в hot path.get()обходит фильтры soft-delete: если строка есть в БД, она вернётся, даже если ваш query-default добавляетWHERE deleted_at IS NULL.- После
commit()все persistent-объекты становятся expired (приexpire_on_commit=True). Следующее обращение к атрибуту вызовет lazy SELECT — неожиданно для кода, который «просто читает поле». session.add()на объект с уже занятым PK (конфликт) упадёт только при flush, а не сразу — ошибка может всплыть далеко от места добавления.- В async-коде (
AsyncSession)get()становитсяawait session.get(...)— синхронные привычки ломают event loop. merge()не подходит для bulk upsert: при 1000 записей это 1000 SELECT + 1000 UPDATE. Используйтеinsert().on_conflict_do_update().
Common mistakes
- Описывать add merge get только как термин и не показывать механизм на минимальном примере.
- Игнорировать ошибки, пустые данные, конкурентный доступ или границы транзакции.
- Не связывать поведение с официальным контрактом SQLAlchemy и реальной эксплуатацией.
What the interviewer is testing
- Объясняет add merge get через последовательность действий, а не через набор ключевых слов.
- Приводит короткий кодовый пример или production-сценарий с ожидаемым поведением.
- Называет хотя бы один риск: производительность, безопасность, транзакции, память или сопровождение.