После commit объект перестал читаться: expire_on_commit SQLAlchemy
Функция сохраняет запись, вызывает commit, закрывает Session и возвращает ORM-объект. HTTP-слой пытается прочитать name и получает DetachedInstanceError. До commit значение точно существовало. Причина в том, что успешная фиксация по умолчанию помечает состояние объектов истёкшим: следующее чтение должно получить актуальные данные из базы, а соединять объект с базой уже некому.
Commit меняет состояние атрибутов
expire_on_commit=True помогает не считать прежнее состояние безусловно актуальным после завершения транзакции. Пока Session открыта, чтение истёкшего атрибута способно выполнить SELECT. После закрытия объект отсоединён и загрузка невозможна. Поэтому возврат ORM-экземпляра через несколько слоёв требует понимания того, какие атрибуты уже загружены и когда они могут истечь.
Надёжный вариант для web-команды — построить обычное представление ответа до закрытия Session. Можно явно вызвать refresh после commit, затем скопировать необходимые поля в словарь или DTO. Так последующая JSON-сериализация не пытается неожиданно обратиться к базе. Представление содержит только то, что действительно требуется клиенту.
Воспроизведём ошибку и два решения
Установите SQLAlchemy. Пример использует временную SQLite. Сначала ORM-объект возвращается после закрытия Session и падает на чтении. Затем функция создаёт готовый ответ внутри Session. В конце показан альтернативный expire_on_commit=False для случая, когда сознательно нужен доступ к сохранённому состоянию.
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
from sqlalchemy.orm.exc import DetachedInstanceError
class Base(DeclarativeBase):
pass
class Item(Base):
__tablename__ = "items"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as db:
detached = Item(name="first")
db.add(detached)
db.commit()
try:
detached.name
except DetachedInstanceError:
print({"after_close": "DetachedInstanceError"})
else:
raise AssertionError("expired detached attribute unexpectedly loaded")
def create_response(name):
with Session(engine) as db:
item = Item(name=name)
db.add(item)
db.commit()
db.refresh(item)
return {"id": item.id, "name": item.name}
response = create_response("second")
assert response["name"] == "second" and response["id"] == 2
with Session(engine, expire_on_commit=False) as db:
retained = Item(name="third")
db.add(retained)
db.commit()
assert retained.name == "third"
print({"response": response, "retained": retained.name})
engine.dispose()
Что меняет отключение expire
expire_on_commit=False сохраняет уже загруженные значения после commit. Это бывает удобно для выбранной архитектуры, но не делает их всегда свежими. Другой запрос мог изменить строку позднее. Также настройка не загружает отношения, к которым раньше не обращались. После закрытия Session незагруженная коллекция всё равно потребует соединение и может упасть.
Поэтому не стоит выключать expire только ради исчезновения исключения, не посмотрев место сериализации. Определите, должен ли слой сервиса возвращать ORM-сущности или готовые результаты команды. Для большинства HTTP-ответов небольшая отдельная модель делает зависимость от базы яснее и уменьшает риск выдачи внутренних полей.
Refresh после commit выполняет новое чтение, что важно учитывать в задержке запроса. Иногда нужные поля уже доступны после flush до commit, но клиенту нельзя сообщать успех до успешной фиксации. Выбор последовательности зависит от серверных значений, триггеров и требований к возвращаемому состоянию. Проверяйте на своей базе, какие значения действительно появляются после записи.
В регрессионном тесте обращайтесь к результату именно после выхода из функции репозитория. Проверка item.name внутри with Session успешно загрузит атрибут и случайно скроет проблему. То же относится к отладочному print перед закрытием: он способен изменить наблюдаемое поведение, выполнив нужную загрузку заранее.
В разборе Session на запрос показана общая организация ресурса. Здесь исследуется конкретное состояние объекта после commit и то, как безопасно передать результат за пределы этого ресурса.
Продолжить на практике
Сделайте команду создания и верните из неё отдельный результат, затем проверьте JSON endpoint. Бесплатный курс FastAPI объединяет SQLAlchemy, модели ответа и тесты — это подходящая практика для такой последовательности сохранения. Откройте бесплатную программу FastAPI.
Продолжить на практике
- FastAPI для начинающих: API с базой данных и тестами ↗
Соберёте свой первый полноценный backend на FastAPI: с базой данных, регистрацией, JWT-авторизацией и автотестами. 34 коротких урока и 140 заданий с проверкой — бесплатно.
