Когда и где закрывать сессию SQLAlchemy
Что проверяют
Понимаете ли вы, что Session это не соединение, а единица работы с картой идентичности, и что её жизнь должна совпадать с границами операции. Следом спрашивают, как это устроено у вас в FastAPI или Celery и откуда берётся DetachedInstanceError.
Как отвечать
Сессия берёт соединение из пула лениво, при первом запросе, и отдаёт его обратно после commit(), rollback() или close(). Метод close() откатывает незавершённую транзакцию, отвязывает все объекты и возвращает соединение в пул. После него той же сессией можно пользоваться снова.
Правило одно: одна сессия на единицу работы. Для веба это запрос, для воркера задача, для скрипта пачка операций.
- контекстный менеджер:
with Session() as session:закрывает сессию, но не коммитит.with Session.begin() as session:коммитит при успехе, откатывает при исключении и закрывает; - FastAPI: зависимость с
yield, внутриwith SessionLocal() as db: yield db. Для async этоasync_sessionmakerиasync with; - Flask-SQLAlchemy: сессия построена на
scoped_session, расширение само вызываетremove()при завершении контекста приложения; - Celery: сессия создаётся внутри задачи и закрывается в её конце, а не живёт глобально в воркере.
Отдельно упомяните expire_on_commit=True, это значение по умолчанию. После коммита атрибуты объектов помечаются устаревшими, и первое обращение снова идёт в базу. Если объект к этому моменту отвязан, получите DetachedInstanceError. В AsyncSession неявная подгрузка падает с MissingGreenlet, поэтому там обычно ставят expire_on_commit=False.
Где спотыкаются
Создают одну сессию на модуль и используют её из разных потоков. Session не потокобезопасна, данные разных запросов перемешиваются.
Закрывают сессию, а потом отдают ORM-объект в сериализатор с ленивыми связями. Нужные связи загружают заранее через selectinload или joinedload.
Держат сессию открытой долго, например на весь цикл воркера. В PostgreSQL соединение висит в состоянии idle in transaction, держит блокировки и мешает очистке старых версий строк.