Основы сессии

Что делает сессия?

В самом общем смысле Session устанавливает все контакты с базой данных и представляет собой «зону хранения» всех объектов, которые вы загрузили или связали с ней в течение ее жизни. Он предоставляет интерфейс, через который выполняются SELECT и другие запросы, возвращающие и модифицирующие ORM-объекты. Сами объекты ORM хранятся внутри Session, в структуре identity map - структуре данных, которая хранит уникальные копии каждого объекта, где «уникальный» означает «только один объект с определенным первичным ключом».

Работа Session начинается в основном без статического состояния. После выполнения запросов или персистирования других объектов он запрашивает ресурс соединения у Engine, связанного с Session, а затем устанавливает транзакцию на этом соединении. Эта транзакция остается в силе до тех пор, пока Session не получит команду на фиксацию или откат транзакции.

Объекты ORM, обслуживаемые Session, имеют такую структуру instrumented, что при изменении атрибута или коллекции в программе на языке Python генерируется событие изменения, которое записывается в Session. При запросе к базе данных или фиксации транзакции Session сначала выполняет промывку всех ожидающих изменений, хранящихся в памяти, в базу данных. Это известно как паттерн unit of work.

При использовании Session полезно рассматривать хранимые в ORM mapped-объекты как прокси-объекты к строкам базы данных, которые локальны для транзакции, проводимой Session. Для поддержания состояния объектов в соответствии с тем, что реально находится в базе данных, существует целый ряд событий, которые заставляют объекты повторно обращаться к базе данных для поддержания синхронизации. Можно «отсоединить» объекты от Session и продолжать их использовать, хотя такая практика имеет свои недостатки. Предполагается, что обычно отсоединенные объекты следует повторно связать с другим Session, когда вы захотите снова работать с ними, чтобы они могли вернуться к своей обычной задаче представления состояния базы данных.

Основы использования сеанса

Здесь представлены самые основные схемы использования Session.

Открытие и закрытие сессии

Session может быть построен самостоятельно или с использованием класса sessionmaker. Обычно ему передается один Engine в качестве источника связности. Типичное использование может выглядеть так:

from sqlalchemy import create_engine
from sqlalchemy.orm import Session

# an Engine, which the Session will use for connection
# resources
engine = create_engine("postgresql+psycopg2://scott:tiger@localhost/")

# create session and add objects
with Session(engine) as session:
    session.add(some_object)
    session.add(some_other_object)
    session.commit()

Выше Session инстанцируется Engine, связанный с конкретным URL базы данных. Затем он используется в менеджере контекста Python (т.е. в операторе with:) таким образом, что автоматически закрывается в конце блока; это эквивалентно вызову метода Session.close().

Вызов Session.commit() необязателен и нужен только в том случае, если в результате работы с Session появились новые данные, которые необходимо персистировать в базу данных. Если бы мы выполняли только вызовы SELECT и не требовали записи изменений, то вызов Session.commit() был бы излишним.

Примечание

Обратите внимание, что после вызова Session.commit() явно или с помощью менеджера контекста все объекты, связанные с Session, становятся expired, то есть их содержимое стирается, чтобы быть загруженным заново в следующей транзакции. Если эти объекты вместо этого будут detached, то они будут нефункциональны до тех пор, пока не будут повторно ассоциированы с новым Session, если только параметр Session.expire_on_commit не используется для отключения такого поведения. Более подробно см. раздел Совершение.

Формирование блока begin / commit / rollback

Мы также можем заключить вызов Session.commit() и общее «обрамление» транзакции в контекстный менеджер для тех случаев, когда мы будем фиксировать данные в базе данных. Под «обрамлением» мы понимаем, что если все операции прошли успешно, то будет вызван метод Session.commit(), но если возникли исключения, то будет вызван метод Session.rollback(), чтобы транзакция была немедленно откачена, прежде чем распространить исключение наружу. В Python это наиболее наглядно выражается с помощью блока try: / except: / else:, например:

# verbose version of what a context manager will do
with Session(engine) as session:
    session.begin()
    try:
        session.add(some_object)
        session.add(some_other_object)
    except:
        session.rollback()
        raise
    else:
        session.commit()

Проиллюстрированную выше длинную последовательность операций можно выполнить более лаконично, используя объект SessionTransaction, возвращаемый методом Session.begin(), который предоставляет интерфейс менеджера контекста для той же последовательности операций:

# create session and add objects
with Session(engine) as session:
    with session.begin():
        session.add(some_object)
        session.add(some_other_object)
    # inner context calls session.commit(), if there were no exceptions
# outer context calls session.close()

В более кратком виде эти два контекста можно объединить:

# create session and add objects
with Session(engine) as session, session.begin():
    session.add(some_object)
    session.add(some_other_object)
# inner context calls session.commit(), if there were no exceptions
# outer context calls session.close()

Использование сеансового устройства

Назначение sessionmaker - предоставить фабрику для Session объектов с фиксированной конфигурацией. Поскольку типично, что приложение будет иметь объект Engine в области видимости модуля, sessionmaker может предоставить фабрику для объектов Session, которые противоречат этому движку:

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# an Engine, which the Session will use for connection
# resources, typically in module scope
engine = create_engine("postgresql+psycopg2://scott:tiger@localhost/")

# a sessionmaker(), also in the same scope as the engine
Session = sessionmaker(engine)

# we can now construct a Session() without needing to pass the
# engine each time
with Session() as session:
    session.add(some_object)
    session.add(some_other_object)
    session.commit()
# closes the session

sessionmaker является аналогом Engine как фабрика уровня модуля для сессий/соединений на уровне функций. Как таковой он также имеет свой метод sessionmaker.begin(), аналогичный Engine.begin(), который возвращает объект Session, а также поддерживает блок begin/commit/rollback:

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# an Engine, which the Session will use for connection
# resources
engine = create_engine("postgresql+psycopg2://scott:tiger@localhost/")

# a sessionmaker(), also in the same scope as the engine
Session = sessionmaker(engine)

# we can now construct a Session() and include begin()/commit()/rollback()
# at once
with Session.begin() as session:
    session.add(some_object)
    session.add(some_other_object)
# commits the transaction, closes the session

Там, где указано выше, транзакция Session будет зафиксирована, а транзакция Session будет закрыта, когда завершится блок with:.

При написании приложения фабрика sessionmaker должна иметь ту же область видимости, что и объект Engine, созданный с помощью create_engine(), которая обычно находится на уровне модуля или в глобальной области видимости. Поскольку оба эти объекта являются фабриками, они могут использоваться одновременно любым количеством функций и потоков.

См.также

sessionmaker

Session

Запрос по адресу

select() Select Session.execute() Session.scalars() Result ScalarResult T

:ref:`queryguide_toplevel`A

from sqlalchemy import select
from sqlalchemy.orm import Session

with Session(engine) as session:
    # query for ``User`` objects
    statement = select(User).filter_by(name="ed")

    # list of ``User`` objects
    user_obj = session.scalars(statement).all()

    # query for individual columns
    statement = select(User.name, User.fullname)

    # list of Row objects
    rows = session.execute(statement).all()

Изменено в версии 2.0: Запросы в стиле «2.0» теперь являются стандартными. Заметки о переходе с серии 1.x см. в 2.0 Миграция - использование ORM.

Добавление новых или существующих элементов

Session.add() используется для размещения экземпляров в сессии. Для экземпляров transient (т.е. совершенно новых) это приведет к тому, что при следующей загрузке будет выполнен INSERT для этих экземпляров. Для экземпляров, которые являются persistent (т.е. были загружены данной сессией), они уже присутствуют и добавлять их не нужно. Экземпляры, которые являются detached (т.е. были удалены из сеанса), могут быть повторно ассоциированы с сеансом с помощью этого метода:

user1 = User(name="user1")
user2 = User(name="user2")
session.add(user1)
session.add(user2)

session.commit()  # write changes to the database

Чтобы добавить в сессию сразу список элементов, используйте Session.add_all():

session.add_all([item1, item2, item3])

Операция Session.add() каскадируется по каскаду save-update. Подробнее об этом см. раздел Каскады.

Удаление

Метод Session.delete() помещает экземпляр в список объектов сессии, которые должны быть помечены как удаленные:

# mark two objects to be deleted
session.delete(obj1)
session.delete(obj2)

# commit (or flush)
session.commit()

Session.delete() помечает объект на удаление, в результате чего для каждого затронутого первичного ключа будет выдан оператор DELETE. До удаления объекты, помеченные «delete», присутствуют в коллекции Session.deleted. После DELETE они исключаются из коллекции Session, которая становится постоянной после фиксации транзакции.

Операция Session.delete() связана с различными важными особенностями поведения, в частности, с тем, как обрабатываются отношения с другими объектами и коллекциями. Подробнее об этом говорится в разделе Каскады, но в целом правила таковы:

  • Строки, соответствующие сопоставленным объектам, связанным с удаляемым объектом с помощью директивы relationship(), по умолчанию не удаляются. Если эти объекты имеют ограничение внешнего ключа, обратное удаляемой строке, то эти столбцы устанавливаются в NULL. Это приведет к нарушению ограничений, если столбцы являются ненулевыми.

  • Чтобы изменить «SET NULL» на DELETE строки связанного объекта, используйте каскад удалить на relationship().

  • Строки, находящиеся в таблицах, связанных как таблицы «многие-ко-многим», через параметр relationship.secondary, удаляются во всех случаях при удалении объекта, на который они ссылаются.

  • Если связанные объекты включают ограничение внешнего ключа, обратное удаляемому объекту, и связанные коллекции, к которым они принадлежат, в данный момент не загружены в память, то единица работы выполнит SELECT для получения всех связанных строк, чтобы значения их первичных ключей могли быть использованы для выполнения операторов UPDATE или DELETE для этих связанных строк. Таким образом, ORM без дополнительных инструкций выполнит функцию ON DELETE CASCADE, даже если она настроена на объекты Core ForeignKeyConstraint.

  • Параметр relationship.passive_deletes может быть использован для настройки этого поведения и более естественного использования «ON DELETE CASCADE»; при установке значения True эта операция SELECT больше не будет выполняться, однако строки, присутствующие локально, все равно будут подвергаться явному SET NULL или DELETE. Установка значения relationship.passive_deletes в строку "all" отключает все связанные с обновлением/удалением объектов.

  • Когда для объекта, помеченного на удаление, происходит DELETE, этот объект не удаляется автоматически из коллекций или ссылок на объекты, которые на него ссылаются. По истечении срока действия Session эти коллекции могут быть загружены вновь, так что объект в них уже не будет присутствовать. Однако предпочтительнее вместо использования Session.delete() для этих объектов удалить объект из его коллекции, а затем использовать delete-orphan, чтобы он был удален как вторичный эффект удаления коллекции. Пример этого см. в разделе Примечания по удалению - Удаление объектов, на которые ссылаются коллекции и скалярные отношения.

См.также

удалить - описывает «каскад удаления», который отмечает связанные объекты для удаления при удалении ведущего объекта.

delete-orphan - описывает «каскад удаления сирот», который помечает связанные объекты на удаление, когда они отвязываются от своего ведущего объекта.

Примечания по удалению - Удаление объектов, на которые ссылаются коллекции и скалярные отношения - важный фон для Session.delete(), так как предполагает освежение в памяти отношений.

Промывка

При использовании Session в конфигурации по умолчанию шаг промывки почти всегда выполняется прозрачно. В частности, промывка происходит до выдачи любого отдельного оператора SQL в результате вызова Query или 2.0-style Session.execute(), а также в рамках вызова Session.commit() до фиксации транзакции. Она также происходит перед выдачей SAVEPOINT при использовании Session.begin_nested().

Принудительный сброс Session может быть осуществлен в любой момент путем вызова метода Session.flush():

session.flush()

Промывка, которая происходит автоматически в области действия некоторых методов, называется autoflush. Autoflush определяется как настраиваемый автоматический вызов смыва, который происходит в начале методов, включая:

  • Session.execute() и другие методы SQL-выполнения, когда они используются против SQL-конструкций с поддержкой ORM, таких как select() объекты, которые ссылаются на сущности ORM и/или атрибуты ORM-mapped

  • Когда вызывается Query для отправки SQL в базу данных

  • Внутри метода Session.merge() перед запросом к базе данных

  • Когда объекты находятся в состоянии refreshed

  • Когда операции ORM lazy load происходят над атрибутами незагруженного объекта.

Существуют также точки, в которых слив происходит безусловно; эти точки находятся в пределах ключевых границ транзакций, к которым относятся:

Поведение autoflush, применительно к предыдущему списку элементов, может быть отключено путем построения Session или sessionmaker с передачей параметра Session.autoflush в виде False:

Session = sessionmaker(autoflush=False)

Кроме того, автопромывка может быть временно отключена в потоке использования Session с помощью менеджера контекста Session.no_autoflush:

with mysession.no_autoflush:
    mysession.add(some_object)
    mysession.flush()

Повторим еще раз: Процесс промывки всегда происходит при вызове транзакционных методов, таких как Session.commit() и Session.begin_nested(), независимо от каких-либо настроек «автопромывки», когда у Session остаются не обработанные изменения.

Поскольку Session вызывает SQL к базе данных только в контексте транзакции DBAPI, все операции «flush» сами по себе происходят только в рамках транзакции базы данных (с учетом isolation level транзакции базы данных), при условии, что DBAPI не находится в режиме driver level autocommit. Это означает, что если соединение с базой данных предусматривает atomicity в своих транзакционных настройках, то при неудаче любого отдельного DML-оператора внутри flush вся операция будет откачена.

Если сбой произошел внутри flush, то для продолжения использования той же Session требуется явный вызов Session.rollback() после сбоя flush, даже если основная транзакция уже была откачена (даже если драйвер базы данных технически находится в режиме автокоммита на уровне драйвера). Это делается для того, чтобы последовательно сохранялась общая схема вложенности так называемых «субтранзакций». Более подробное описание такого поведения содержится в разделе FAQ «Транзакция этого сеанса была откачена из-за предыдущего исключения во время промывки.» (или аналогично).

См.также

«Транзакция этого сеанса была откачена из-за предыдущего исключения во время промывки.» (или аналогично) - дополнительная информация о том, почему Session.rollback() должен вызываться при неудачном flush.

Получение по первичному ключу

Поскольку в Session используется identity map, который ссылается на текущие объекты в памяти по первичному ключу, то метод Session.get() предоставляется как средство нахождения объектов по первичному ключу, сначала ища в текущей карте идентификаторов, а затем запрашивая базу данных на предмет отсутствия текущих значений. Например, чтобы найти объект User с первичным ключом (5, ):

my_user = session.get(User, 5)

В Session.get() также включены формы вызова для составных значений первичного ключа, которые могут передаваться в виде кортежей или словарей, а также дополнительные параметры, позволяющие использовать специфические опции загрузчика и исполнения. Полный список параметров приведен в разделе Session.get().

См.также

Session.get()

Истечение срока действия / Обновление

Важным моментом, который часто возникает при использовании Session, является работа с состоянием объектов, загруженных из базы данных, с точки зрения их синхронизации с текущим состоянием транзакции. В основе SQLAlchemy ORM лежит концепция identity map, согласно которой при «загрузке» объекта из SQL-запроса сохраняется уникальный экземпляр объекта Python, соответствующий определенному идентификатору базы данных. Это означает, что если мы выполним два отдельных запроса, каждый из которых будет касаться одной и той же строки, и получим в ответ сопоставленный объект, то оба запроса вернут один и тот же объект Python:

>>> u1 = session.scalars(select(User).where(User.id == 5)).one()
>>> u2 = session.scalars(select(User).where(User.id == 5)).one()
>>> u1 is u2
True

Из этого следует, что когда ORM получает строки из запроса, он пропускает заполнение атрибутов для объекта, который уже загружен. При проектировании предполагается, что транзакция идеально изолирована, а в случае, когда транзакция не изолирована, приложение может по мере необходимости выполнять действия по обновлению объектов из транзакции базы данных. Более подробно эта концепция рассматривается в статье FAQ по адресу Я перезагружаю данные с помощью Session, но он не видит изменений, которые я сделал в другом месте..

Когда объект, отображенный в ORM, загружается в память, существует три общих способа обновить его содержимое новыми данными из текущей транзакции:

  • метод expire() - метод Session.expire() стирает содержимое выбранных или всех атрибутов объекта таким образом, что они будут загружены из базы данных при следующем обращении к ним, например, по шаблону lazy loading:

    session.expire(u1)
    u1.some_attribute  # <-- lazy loads from the transaction
  • метод refresh() - тесно связан с методом Session.refresh(), который делает все то же самое, что и метод Session.expire(), но при этом сразу же выдает один или несколько SQL-запросов, чтобы фактически обновить содержимое объекта:

    session.refresh(u1)  # <-- emits a SQL query
    u1.some_attribute  # <-- is refreshed from the transaction
  • метод populate_existing() или опция выполнения - теперь это опция выполнения, документированная по адресу Заполнить существующие; в устаревшей форме она находится на объекте Query в виде метода Query.populate_existing(). Эта операция в любом виде указывает на то, что возвращаемые из запроса объекты должны быть безусловно переполнены из их содержимого в базе данных:

    u2 = session.scalars(
        select(User).where(User.id == 5).execution_options(populate_existing=True)
    ).one()

Дальнейшее обсуждение концепции refresh / expire можно найти в Обновление / истечение срока действия.

UPDATE и DELETE с произвольным предложением WHERE

В SQLAlchemy 2.0 расширены возможности по созданию нескольких разновидностей ORM-операторов INSERT, UPDATE и DELETE. Документация приведена в документе по адресу Операции INSERT, UPDATE и DELETE с поддержкой ORM.

Автозапуск

Для объекта Session характерно поведение, известное как autobegin. Это означает, что Session будет внутренне считать себя находящимся в «транзакционном» состоянии, как только с Session будет выполнена какая-либо работа, связанная либо с модификацией внутреннего состояния Session в связи с изменением состояния объекта, либо с операциями, требующими подключения к базе данных.

При первом построении Session транзакционное состояние отсутствует. Транзакционное состояние запускается автоматически, при вызове метода Session.add() или Session.execute(), или, аналогично, при выполнении Query для возврата результатов (что в конечном итоге использует Session.execute()), или при изменении атрибута в объекте persistent.

Состояние транзакций можно проверить, обратившись к методу Session.in_transaction(), который возвращает True или False, указывая, был ли выполнен шаг «autobegin». Метод Session.get_transaction() обычно не нужен, но он возвращает реальный объект SessionTransaction, представляющий данное транзакционное состояние.

Транзакционное состояние Session может быть также запущено явно, путем вызова метода Session.begin(). При вызове этого метода Session переходит в состояние «транзакционный» безусловно. Session.begin() может быть использован в качестве менеджера контекста, как описано в Формирование блока begin / commit / rollback.

Отключение автозапуска для предотвращения неявных транзакций

Поведение «автозапуска» может быть отключено с помощью параметра Session.autobegin, установленного в значение False. При использовании этого параметра Session будет требовать явного вызова метода Session.begin(). При построении, а также после вызова любого из методов Session.rollback(), Session.commit() или Session.close(), Session не будет неявно начинать новые транзакции и выдаст ошибку, если попытка использования Session будет предпринята без предварительного вызова Session.begin():

with Session(engine, autobegin=False) as session:
    session.begin()  # <-- required, else InvalidRequestError raised on next call

    session.add(User(name="u1"))
    session.commit()

    session.begin()  # <-- required, else InvalidRequestError raised on next call

    u1 = session.scalar(select(User).filter_by(name="u1"))

Добавлено в версии 2.0: Добавлено Session.autobegin, позволяющее отключить поведение «автозапуска»

Совершение

Session.commit() используется для фиксации текущей транзакции. По своей сути это означает, что он испускает COMMIT на всех текущих соединениях с базой данных, в которых выполняется транзакция; с точки зрения DBAPI это означает, что метод connection.commit() DBAPI вызывается на каждом соединении DBAPI.

Если для Session не существует транзакции, указывающей на то, что с момента предыдущего вызова Session.commit() для этого Session не было вызвано никаких операций, метод начнет и зафиксирует только внутреннюю «логическую» транзакцию, которая обычно не влияет на базу данных, если только не были обнаружены ожидающие изменения флеша, но все равно вызовет обработчики событий и правила истечения срока действия объектов.

Операция Session.commit() безоговорочно выдает Session.flush() перед выполнением COMMIT на соответствующих соединениях с базой данных. Если не обнаружено никаких ожидающих изменений, то SQL в базу данных не выдается. Это поведение не настраивается и не зависит от параметра Session.autoflush.

После этого Session.commit() выполнит COMMIT фактической транзакции базы данных или транзакций, если таковые имеются.

Наконец, при завершении транзакции все объекты, находящиеся внутри Session, становятся expired. Это делается для того, чтобы при следующем обращении к экземплярам, будь то обращение к атрибутам или их присутствие в результате SELECT, они получали самое последнее состояние. Такое поведение может контролироваться флагом Session.expire_on_commit, который может быть установлен в значение False, если такое поведение нежелательно.

См.также

Автозапуск

Откат

Session.rollback() откатывает текущую транзакцию, если таковая имеется. Если транзакция отсутствует, то метод проходит без звука.

При стандартной конфигурации сеанса состояние сеанса после начала транзакции через autobegin или путем явного вызова метода Session.begin() выглядит следующим образом:

  • Все транзакции откатываются, а все соединения возвращаются в пул соединений, если только сессия не была привязана непосредственно к соединению (Connection), в этом случае соединение сохраняется (но все равно откатывается).

  • Объекты, которые изначально находились в состоянии pending при добавлении их в Session в течение жизни транзакции, удаляются, что соответствует откату оператора INSERT. При этом состояние их атрибутов остается неизменным.

  • Объекты, которые были помечены как deleted в течение жизни транзакции, переводятся обратно в состояние persistent, что соответствует откату оператора DELETE. Обратите внимание, что если эти объекты были сначала помечены как pending в транзакции, то приоритет имеет эта операция.

  • Все объекты, которые не были удалены, являются полностью истекшими - это происходит независимо от установки Session.expire_on_commit.

Поняв это состояние, Session может спокойно продолжать работу после отката.

Изменено в версии 1.4: Объект Session теперь имеет отложенное поведение «begin», как описано в autobegin. Если транзакция не начата, то методы Session.commit() и Session.rollback() не имеют никакого эффекта. До версии 1.4 такое поведение не наблюдалось, поскольку в режиме неавтокоммита транзакция всегда неявно присутствовала.

Когда Session.flush() терпит неудачу, обычно по таким причинам, как нарушение ограничений первичного ключа, внешнего ключа или ограничения «not nullable», автоматически выдается сообщение ROLLBACK (в настоящее время продолжение работы после частичного сбоя невозможно). Однако в этот момент Session переходит в состояние, известное как «неактивное», и вызывающее приложение всегда должно явно вызывать метод Session.rollback(), чтобы Session снова перешел в пригодное для использования состояние (его также можно просто закрыть и выбросить). Более подробно об этом см. в разделе FAQ по адресу «Транзакция этого сеанса была откачена из-за предыдущего исключения во время промывки.» (или аналогично).

См.также

Автозапуск

Закрытие

Метод Session.close() выдает команду Session.expunge_all(), которая удаляет из сессии все объекты ORM-mapped, а также releases любые транзакционные ресурсы/ресурсы соединений из объекта(ов) Engine, с которым он связан. Когда соединения возвращаются в пул соединений, транзакционное состояние также откатывается.

Когда Session закрыт, он, по сути, находится в исходном состоянии, в котором он был создан, и может быть использован снова. В этом смысле метод Session.close() больше похож на «сброс» в чистое состояние, а не на «закрытие базы данных».

Рекомендуется ограничивать область действия Session вызовом Session.close() в конце, особенно если не используются методы Session.commit() или Session.rollback(). В качестве менеджера контекста можно использовать Session, чтобы обеспечить вызов Session.close():

with Session(engine) as session:
    result = session.execute(select(User))

# closes session automatically

Изменено в версии 1.4: Объект Session имеет отложенное поведение «begin», описанное в autobegin. больше не начинает немедленно новую транзакцию после вызова метода Session.close().

Часто задаваемые вопросы

К этому моменту у многих пользователей уже возникают вопросы по работе с сессиями. В этом разделе представлен мини-FAQ (заметим, что у нас есть и real FAQ) по самым основным вопросам, с которыми приходится сталкиваться при использовании Session.

Когда нужно сделать sessionmaker?

Только один раз, где-то в глобальной области видимости вашего приложения. Это должно рассматриваться как часть конфигурации вашего приложения. Если ваше приложение имеет три файла .py в пакете, то вы можете, например, поместить строку sessionmaker в ваш файл __init__.py; с этого момента ваши другие модули говорят: «from mypackage import Session». Таким образом, все остальные модули просто используют Session(), а конфигурация сессии управляется из этой центральной точки.

Если ваше приложение запускается, выполняет импорт, но не знает, к какой базе данных оно будет подключаться, вы можете привязать Session на уровне «класса» к движку позже, используя sessionmaker.configure().

В примерах этого раздела мы часто будем показывать, что sessionmaker создается прямо над строкой, где мы вызываем Session. Но это только для примера! В реальности sessionmaker будет находиться где-то на уровне модуля. Вызовы инстанцирования Session будут располагаться в той точке приложения, где начинаются обращения к базе данных.

Когда я строю Session, когда фиксирую его и когда закрываю?

Session обычно строится в начале логической операции, где потенциально предполагается обращение к базе данных.

Session, когда бы он ни использовался для связи с базой данных, начинает транзакцию с базой данных, как только начинает взаимодействовать с ней. Эта транзакция продолжается до тех пор, пока Session не будет откачена, зафиксирована или закрыта. При повторном использовании Session после завершения предыдущей транзакции начинает новую транзакцию; из этого следует, что Session может иметь продолжительность жизни в течение многих транзакций, хотя и только одной за один раз. Мы называем эти два понятия объем транзакции и объем сессии.

Определить оптимальные точки начала и конца области видимости Session обычно несложно, хотя большое разнообразие возможных архитектур приложений может создавать сложные ситуации.

Некоторые примерные сценарии включают:

  • Веб-приложения. В этом случае лучше всего воспользоваться интеграциями SQLAlchemy, предоставляемыми используемым веб-фреймворком. В противном случае основной схемой является создание Session в начале веб-запроса, вызов метода Session.commit() в конце веб-запросов, выполняющих POST, PUT или DELETE, и закрытие сессии в конце веб-запроса. Также обычно рекомендуется установить значение Session.expire_on_commit в False, чтобы при последующем доступе к объектам, полученным от Session в слое представления, не нужно было выполнять новые SQL-запросы для обновления объектов, если транзакция уже была зафиксирована.

  • Фоновый демон, порождающий дочерние форки, должен создавать локальный Session для каждого дочернего процесса, работать с ним Session в течение всего времени выполнения «задания», которое обрабатывает форк, а затем уничтожать его по завершении работы.

  • Для сценария командной строки приложение создает единственную глобальную переменную Session, которая устанавливается, когда программа начинает выполнять свою работу, и фиксирует ее сразу после того, как программа завершает свою задачу.

  • Для приложения, управляемого графическим интерфейсом, область действия Session лучше всего может находиться в пределах генерируемого пользователем события, например, нажатия кнопки. Или же область действия может соответствовать явному взаимодействию с пользователем, например, пользователь «открывает» серию записей, а затем «сохраняет» их.

Как правило, приложение должно управлять жизненным циклом сессии внешне по отношению к функциям, работающим с конкретными данными. Это фундаментальное разделение интересов, которое позволяет сохранить независимость операций с конкретными данными от контекста, в котором они осуществляют доступ к этим данным и манипулируют ими.

Например, не делайте этого:

### this is the **wrong way to do it** ###


class ThingOne:
    def go(self):
        session = Session()
        try:
            session.execute(update(FooBar).values(x=5))
            session.commit()
        except:
            session.rollback()
            raise


class ThingTwo:
    def go(self):
        session = Session()
        try:
            session.execute(update(Widget).values(q=18))
            session.commit()
        except:
            session.rollback()
            raise


def run_my_program():
    ThingOne().go()
    ThingTwo().go()

Сохранять жизненный цикл сессии (и, как правило, транзакции) отдельным и внешним. Приведенный ниже пример иллюстрирует, как это может выглядеть, и дополнительно использует менеджер контекста Python (т.е. ключевое слово with:) для автоматического управления областью действия Session и его транзакцией:

### this is a **better** (but not the only) way to do it ###


class ThingOne:
    def go(self, session):
        session.execute(update(FooBar).values(x=5))


class ThingTwo:
    def go(self, session):
        session.execute(update(Widget).values(q=18))


def run_my_program():
    with Session() as session:
        with session.begin():
            ThingOne().go(session)
            ThingTwo().go(session)

Изменено в версии 1.4: Session может использоваться в качестве менеджера контекста без применения внешних вспомогательных функций.

Является ли сессия кэшем?

Да… нет. Он в некоторой степени используется как кэш, поскольку реализует шаблон identity map и хранит объекты по первичному ключу. Однако он не выполняет никакого кэширования запросов. Это означает, что если вы говорите session.scalars(select(Foo).filter_by(name='bar')), то даже если Foo(name='bar') находится прямо здесь, в карте идентификации, сессия не имеет об этом никакого представления. Она должна отправить SQL-запрос к базе данных, получить строки обратно, а затем, когда она увидит первичный ключ в строке, тогда она может посмотреть в локальную карту идентификации и увидеть, что объект уже там. Только когда вы говорите query.get({some primary key}), Session не должен выдавать запрос.

Кроме того, по умолчанию сессия хранит экземпляры объектов, используя слабую ссылку. Это также не позволяет использовать сессию в качестве кэша.

Session не предназначен для того, чтобы быть глобальным объектом, к которому все обращаются как к «реестру» объектов. Это скорее задача кэша второго уровня. SQLAlchemy предоставляет шаблон для реализации кэширования второго уровня с использованием dogpile.cache на примере Кэширование Dogpile.

Как получить Session для определенного объекта?

Используйте метод класса Session.object_session(), доступный на Session:

session = Session.object_session(someobject)

Также может быть использована более новая система API для проверки во время выполнения:

from sqlalchemy import inspect

session = inspect(someobject).session

Является ли сессия потокобезопасной?

Session в значительной степени предназначен для использования в непоследовательном режиме, что обычно означает использование только в одном потоке одновременно.

Session следует использовать таким образом, чтобы для одной серии операций в рамках одной транзакции существовал один экземпляр. Одним из целесообразных способов достижения этого эффекта является ассоциирование Session с текущим потоком (см. раздел Контекстные/поточно-локальные сеансы). Другой способ - использовать схему, в которой Session передается между функциями и в остальном не используется совместно с другими потоками.

Важнее то, что вы не должны хотеть использовать сессию с несколькими параллельными потоками. Это все равно, что в ресторане все будут есть из одной тарелки. Сессия - это локальное «рабочее пространство», которое вы используете для выполнения определенного набора задач; вы не хотите и не должны использовать эту сессию совместно с другими потоками, которые выполняют какую-то другую задачу.

Убедиться в том, что Session используется только в одном потоке одновременно, - это называется подходом к параллелизму «ничего не разделять». Однако на самом деле отсутствие совместного использования Session подразумевает более существенную закономерность: оно означает, что не только сам объект Session, но и все объекты, связанные с этим сеансом, должны находиться в области видимости одного параллельного потока. Набор отображаемых объектов, связанных с Session, по сути, является прокси для данных в строках базы данных, доступ к которым осуществляется через соединение с базой данных, поэтому, как и сам Session, весь набор объектов представляет собой лишь масштабный прокси для соединения (или соединений) с базой данных. В конечном счете, мы не допускаем одновременного доступа к DBAPI-соединению, но поскольку Session и все связанные с ним объекты являются прокси для этого DBAPI-соединения, то весь граф, по сути, небезопасен для одновременного доступа.

Если в одной задаче участвует несколько потоков, то можно рассмотреть возможность разделения сессии и ее объектов между этими потоками; однако в этом крайне необычном случае приложение должно обеспечить надлежащую схему блокировки, чтобы не было последовательного доступа к Session или его состоянию. Более распространенным подходом к данной ситуации является ведение одного Session на каждый параллельный поток, а вместо этого копирование объектов из одного Session в другой, часто используя метод Session.merge() для копирования состояния объекта в новый объект, локальный для другого Session.

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