SQLAlchemyJuniorTechnical

В чём разница между 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-сценарий с ожидаемым поведением.
  • Называет хотя бы один риск: производительность, безопасность, транзакции, память или сопровождение.

Sources

Related topics