SQLAlchemyMiddleTechnical

Что такое scoped_session и зачем он используется в веб-фреймворках, таких как Flask?

scoped_session — реестр, возвращающий одну сессию на поток (или другой scope). Используется во Flask/WSGI для доступа к 'текущей' сессии без передачи явно. В async-приложениях не применяется.

scoped_session в SQLAlchemy

scoped_session — это реестр сессий, который возвращает одну и ту же сессию для одного и того же контекста (по умолчанию — потока). Это позволяет нескольким частям приложения обращаться к «текущей» сессии, не передавая её явно.

Как работает

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, scoped_session

engine = create_engine("postgresql+psycopg2://user:pass@localhost/db")
SessionFactory = sessionmaker(bind=engine)

# scoped_session оборачивает фабрику
db_session = scoped_session(SessionFactory)

# В любом месте приложения — получаем ту же сессию для текущего потока
def get_users():
    return db_session.query(User).all()   # legacy-API, но работает

def create_user(name: str):
    user = User(name=name)
    db_session.add(user)
    db_session.commit()

# В конце запроса — обязательно убираем сессию из реестра
def teardown():
    db_session.remove()   # закрывает сессию и убирает из registry

Использование в Flask

from flask import Flask
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, scoped_session

app = Flask(__name__)
engine = create_engine(app.config["DATABASE_URL"])
db_session = scoped_session(sessionmaker(bind=engine))

@app.teardown_appcontext
def shutdown_session(exception=None):
    db_session.remove()   # критически важно — иначе соединения утекут

@app.route("/users")
def list_users():
    return [{"id": u.id} for u in db_session.query(User).all()]

Кастомный scope (не по потоку)

import threading

# По умолчанию scope = threading.get_ident()
# Можно передать собственную функцию — например, ID запроса
from contextvars import ContextVar

_request_id: ContextVar[int] = ContextVar("request_id")

def request_scope() -> int:
    return _request_id.get()

db_session = scoped_session(SessionFactory, scopefunc=request_scope)

scoped_session vs sessionmaker в современном коде

  • Flask + WSGI (многопоточный) → scoped_session по-прежнему актуален.
  • FastAPI / async → используйте AsyncSession через dependency injection (Depends); scoped_session там не нужен.
  • Flask-SQLAlchemy 3.x управляет сессией сам через app.teardown_appcontext.

Подводные камни

  • Забыть db_session.remove(): сессия не закрывается, соединение с БД висит. В долгоживущих потоках это приводит к исчерпанию пула.
  • Многопоточность без scoped_session: одна сессия не безопасна для нескольких потоков — операции над объектами corrupted state.
  • scoped_session в async: scoped_session использует threading.get_ident по умолчанию — в asyncio все корутины выполняются в одном потоке, поэтому разные запросы получат одну сессию. Это критическая ошибка.
  • Прокси-интерфейс: db_session — прокси, который делегирует к реальной сессии. Его можно передавать в функции, но нельзя сохранять результат db_session() и использовать долго.
  • remove() vs close(): close() закрывает сессию, но оставляет её в реестре. remove() закрывает и удаляет из реестра — следующий вызов создаст новую.

Common mistakes

  • Описывать scoped session только как термин и не показывать механизм на минимальном примере.
  • Игнорировать ошибки, пустые данные, конкурентный доступ или границы транзакции.
  • Не связывать поведение с официальным контрактом SQLAlchemy и реальной эксплуатацией.

What the interviewer is testing

  • Объясняет scoped session через последовательность действий, а не через набор ключевых слов.
  • Приводит короткий кодовый пример или production-сценарий с ожидаемым поведением.
  • Называет хотя бы один риск: производительность, безопасность, транзакции, память или сопровождение.

Sources

Related topics