Контекстные/поточно-локальные сеансы

Помните, в разделе Когда я строю 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 с веб-приложением имеет ровно два требования:

  1. Создайте единственный реестр scoped_session при первом запуске веб-приложения, гарантируя, что этот объект будет доступен остальной части приложения.

  2. Убедитесь, что 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. В этом случае особенно важно, чтобы была реализована надежная система «удаления», поскольку этот словарь не является самоуправляемым.

API контекстной сессии

Вернуться на верх