Где в приложении создавать и хранить экземпляр класса соединения с базой данных
Что проверяют
Понимаете ли вы разницу между пулом соединений, который живёт весь срок процесса, и отдельным соединением или сессией на один запрос. Следом часто спрашивают, что станет с вашим решением под gunicorn с несколькими воркерами или в многопоточном коде.
Как отвечать
Правило такое: один раз на процесс создают то, что дорого и потокобезопасно, а на каждый запрос выдают то, что дёшево и не делится.
- engine или пул: создаётся при старте приложения, в одной точке сборки. В SQLAlchemy это
create_engine, внутри у него пул (QueuePool, по умолчаниюpool_size=5иmax_overflow=10). Для asyncpg этоasyncpg.create_pool; - сессия или соединение: берётся из пула на запрос или единицу работы и возвращается после. В FastAPI это зависимость с
yield, а пул создаётся и закрывается вlifespanи хранится вapp.state. В Django соединениями управляет сам фреймворк,CONN_MAX_AGEрешает, переиспользовать ли их между запросами; - передача: через внедрение зависимостей, а не через глобальный синглтон. Тогда в тестах легко подставить другую базу или транзакцию с откатом.
Для скриптов и CLI достаточно создать соединение в main и передавать вниз. Опция pool_pre_ping=True спасает от соединений, которые база или балансировщик закрыли по таймауту.
Где спотыкаются
Предлагают синглтон с одним соединением на всё приложение. В веб-сервере с потоками запросы начнут делить одну транзакцию, а sqlite3 по умолчанию вообще запретит пользоваться соединением из другого потока (check_same_thread=True).
Считают, что with sqlite3.connect(...) as conn закрывает соединение. Этот контекстный менеджер только коммитит или откатывает транзакцию, закрывать нужно явно или через contextlib.closing.
Открывают соединения в мастер-процессе до fork, например с --preload в gunicorn. Воркеры наследуют одни и те же сокеты и портят друг другу протокол. Пул создают уже в воркере или вызывают engine.dispose(close=False) в дочернем процессе.