Контекстные/поточно-локальные сеансы¶
Помните, в разделе Когда я строю Session, когда я фиксирую его и когда я закрываю его? было введено понятие «области действия сеанса» с акцентом на веб-приложения и практику связывания области действия Session
с областью действия веб-запроса. Большинство современных веб-фреймворков включают инструменты интеграции, позволяющие автоматически управлять областью видимости Session
, и эти инструменты следует использовать по мере их появления.
SQLAlchemy включает собственный объект-помощник, который помогает в создании определяемых пользователем диапазонов Session
. Он также используется сторонними интеграционными системами для построения своих интеграционных схем.
Объект является объектом scoped_session
и представляет собой реестр объектов Session
. Если вы не знакомы с шаблоном реестра, хорошее введение можно найти в Patterns of Enterprise Architecture.
Предупреждение
Реестр scoped_session
по умолчанию использует Python threading.local()
для отслеживания экземпляров Session
. Это не обязательно совместимо со всеми серверами приложений, особенно с теми, которые используют greenlets или другие альтернативные формы контроля параллелизма, что может привести к условиям гонки (например, случайно возникающим сбоям) при использовании в сценариях с умеренным и высоким параллелизмом. Пожалуйста, прочитайте Нить-локальный прицел и Использование Thread-Local Scope в веб-приложениях ниже для более полного понимания последствий использования threading.local()
для отслеживания объектов Session
и рассмотрите более явные способы определения масштаба при использовании серверов приложений, не основанных на традиционных потоках.
Примечание
Объект scoped_session
является очень популярным и полезным объектом, используемым многими приложениями SQLAlchemy. Однако важно отметить, что он представляет только один подход к вопросу управления Session
. Если вы новичок в SQLAlchemy, и особенно если термин «потоково-локальная переменная» кажется вам странным, мы рекомендуем вам, по возможности, сначала ознакомиться с готовой системой интеграции, такой как Flask-SQLAlchemy или zope.sqlalchemy.
A scoped_session
is constructed by calling it, passing it a
factory which can create new Session
objects. A factory
is just something that produces a new object when called, and in the
case of Session
, the most common factory is the sessionmaker
,
introduced earlier in this section. Below we illustrate this usage:
>>> from sqlalchemy.orm import scoped_session
>>> from sqlalchemy.orm import sessionmaker
>>> session_factory = sessionmaker(bind=some_engine)
>>> Session = scoped_session(session_factory)
Созданный нами объект scoped_session
теперь будет обращаться к sessionmaker
при «вызове» реестра:
>>> some_session = Session()
Выше, some_session
является экземпляром Session
, который мы теперь можем использовать для общения с базой данных. Этот же Session
присутствует и в созданном нами реестре scoped_session
. Если мы обратимся к реестру во второй раз, то получим в ответ тот же Session
:
>>> some_other_session = Session()
>>> some_session is some_other_session
True
Этот шаблон позволяет разным частям приложения обращаться к глобальному scoped_session
, так что все эти части могут использовать одну и ту же сессию без необходимости передавать ее в явном виде. Session
, который мы создали в нашем реестре, будет оставаться до тех пор, пока мы явно не скажем нашему реестру утилизировать его, вызвав scoped_session.remove()
:
>>> Session.remove()
Метод scoped_session.remove()
сначала вызывает Session.close()
на текущем Session
, что имеет эффект освобождения любых соединений/транзакционных ресурсов, принадлежащих сначала Session
, а затем отбрасывает сам Session
. «Освобождение» здесь означает, что соединения возвращаются в свой пул соединений, а все транзакционные состояния откатываются назад, в конечном счете, с использованием метода rollback()
основного соединения DBAPI.
В этот момент объект scoped_session
является «пустым» и при повторном вызове создаст новый Session
. Как показано ниже, это уже не тот Session
, который мы имели раньше:
>>> new_session = Session()
>>> new_session is some_session
False
Приведенная выше серия шагов в двух словах иллюстрирует идею шаблона «реестр». Имея на руках эту основную идею, мы можем обсудить некоторые детали работы этого шаблона.
Неявный доступ к методу¶
Задача scoped_session
проста: хранить Session
для всех, кто его запрашивает. В качестве средства более прозрачного доступа к этому Session
, scoped_session
также включает прокси-поведение, что означает, что с самим реестром можно обращаться как с Session
непосредственно; когда методы вызываются на этом объекте, они проксируются на базовый Session
, поддерживаемый реестром:
Session = scoped_session(some_factory)
# equivalent to:
#
# session = Session()
# print(session.query(MyClass).all())
#
print(Session.query(MyClass).all())
Приведенный выше код решает ту же задачу, что и получение текущего Session
путем обращения к реестру, а затем использование этого Session
.
Нить-локальный прицел¶
Пользователи, знакомые с многопоточным программированием, заметят, что представление чего-либо в виде глобальной переменной обычно является плохой идеей, поскольку это подразумевает, что доступ к глобальному объекту будет осуществляться одновременно во многих потоках. Объект Session
полностью предназначен для использования непоследовательно, что в терминах многопоточного программирования означает «только в одном потоке за раз». Поэтому наш вышеприведенный пример использования scoped_session
, где один и тот же объект Session
поддерживается в нескольких вызовах, предполагает, что необходимо предусмотреть некоторый процесс, чтобы несколько вызовов во многих потоках не получали доступа к одной и той же сессии. Мы называем это понятие поточное локальное хранилище, что означает, что используется специальный объект, который будет поддерживать отдельный объект для каждого потока приложения. Python предоставляет такую возможность с помощью конструкции threading.local(). Объект scoped_session
по умолчанию использует этот объект в качестве хранилища, так что для всех, кто обращается к реестру Session
, сохраняется один объект scoped_session
, но только в рамках одного потока. Абоненты, обращающиеся к реестру в другом потоке, получают экземпляр Session
, который является локальным для этого другого потока.
Используя эту технику, scoped_session
обеспечивает быстрый и относительно простой (если вы знакомы с локальным хранением потоков) способ предоставления единственного, глобального объекта в приложении, к которому можно обращаться из нескольких потоков.
Метод scoped_session.remove()
, как всегда, удаляет текущий Session
, связанный с потоком, если таковой имеется. Однако одним из преимуществ объекта threading.local()
является то, что если поток приложения завершается, то «хранилище» для этого потока также собирается в мусор. Таким образом, фактически «безопасно» использовать локальную область видимости потока в приложении, которое порождает и уничтожает потоки, без необходимости вызывать scoped_session.remove()
. Однако область видимости самих транзакций, т.е. их завершение с помощью Session.commit()
или Session.rollback()
, обычно остается чем-то, что должно быть явно организовано в соответствующее время, если только приложение не связывает время жизни потока с временем жизни транзакции.
Использование Thread-Local Scope в веб-приложениях¶
Как обсуждалось в разделе Когда я строю Session, когда я фиксирую его и когда я закрываю его?, веб-приложение строится вокруг концепции веб-запроса, и интеграция такого приложения с Session
обычно подразумевает, что Session
будет связан с этим запросом. Как оказалось, большинство веб-фреймворков Python, за заметными исключениями, такими как асинхронные фреймворки Twisted и Tornado, используют потоки простым способом, таким образом, что конкретный веб-запрос принимается, обрабатывается и завершается в рамках одного рабочего потока. Когда запрос завершается, рабочий поток передается в пул рабочих, где он может быть доступен для обработки другого запроса.
Это простое соответствие веб-запроса и потока означает, что ассоциирование Session
с потоком подразумевает, что он также ассоциируется с веб-запросом, выполняемым в этом потоке, и наоборот, при условии, что Session
создается только после начала веб-запроса и уничтожается непосредственно перед его завершением. Поэтому обычной практикой является использование scoped_session
в качестве быстрого способа интеграции Session
с веб-приложением. Приведенная ниже диаграмма последовательности иллюстрирует этот поток:
Web Server Web Framework SQLAlchemy ORM Code
-------------- -------------- ------------------------------
startup -> Web framework # Session registry is established
initializes Session = scoped_session(sessionmaker())
incoming
web request -> web request -> # The registry is *optionally*
starts # called upon explicitly to create
# a Session local to the thread and/or request
Session()
# the Session registry can otherwise
# be used at any time, creating the
# request-local Session() if not present,
# or returning the existing one
Session.query(MyClass) # ...
Session.add(some_object) # ...
# if data was modified, commit the
# transaction
Session.commit()
web request ends -> # the registry is instructed to
# remove the Session
Session.remove()
sends output <-
outgoing web <-
response
Используя вышеприведенный поток, процесс интеграции Session
с веб-приложением имеет ровно два требования:
Создайте единственный реестр
scoped_session
при первом запуске веб-приложения, гарантируя, что этот объект будет доступен остальной части приложения.Убедитесь, что
scoped_session.remove()
вызывается при завершении веб-запроса, обычно это делается путем интеграции с системой событий веб-фреймворка для создания события «on request end».
Как отмечалось ранее, вышеприведенный шаблон является только одним из возможных способов интеграции Session
с веб-фреймворком, который, в частности, делает существенное допущение, что веб-фреймворк ассоциирует веб-запросы с потоками приложения. Однако настоятельно рекомендуется использовать вместо :class:`.scoped_session` инструменты интеграции, поставляемые с самим веб-фреймворком, если таковые имеются.
В частности, хотя использование локального потока может быть удобным, предпочтительно, чтобы Session
был связан непосредственно с запросом, а не с текущим потоком. В следующем разделе о пользовательских диапазонах подробно описана более продвинутая конфигурация, которая может сочетать использование scoped_session
с прямым диапазоном, основанным на запросе, или с любым другим видом диапазона.
Использование созданных пользователем областей видимости¶
Стандартное поведение объекта scoped_session
по умолчанию - «локальная область видимости потока» - является лишь одним из многих вариантов того, как «охватить» объект Session
. Пользовательская область видимости может быть определена на основе любой существующей системы получения «текущей вещи, с которой мы работаем».
Предположим, что веб-фреймворк определяет библиотечную функцию get_current_request()
. Приложение, построенное с использованием этого фреймворка, может вызвать эту функцию в любое время, и результатом будет некий объект Request
, представляющий текущий обрабатываемый запрос. Если объект Request
является хэшируемым, то эта функция может быть легко интегрирована с scoped_session
, чтобы связать Session
с запросом. Ниже мы проиллюстрируем это в сочетании с гипотетическим маркером событий, предоставляемым веб-фреймворком on_request_end
, который позволяет вызывать код всякий раз, когда запрос заканчивается:
from my_web_framework import get_current_request, on_request_end
from sqlalchemy.orm import scoped_session, sessionmaker
Session = scoped_session(sessionmaker(bind=some_engine), scopefunc=get_current_request)
@on_request_end
def remove_session(req):
Session.remove()
Выше мы инстанцируем scoped_session
обычным образом, за исключением того, что мы передаем нашу функцию возврата запроса в качестве «scopefunc». Это инструктирует scoped_session
использовать эту функцию для генерации ключа словаря всякий раз, когда реестр вызывается для возврата текущего Session
. В этом случае особенно важно, чтобы была реализована надежная система «удаления», поскольку этот словарь не является самоуправляемым.