Сессии / запросы

Я повторно загружаю данные с помощью Session, но он не видит изменений, которые я сделал в другом месте.

Основная проблема такого поведения заключается в том, что сессия ведет себя так, как будто транзакция находится в состоянии сериализуемой изоляции, даже если это не так (а обычно это не так). На практике это означает, что сессия не изменяет данные, которые она уже прочитала в рамках транзакции.

Если термин «уровень изоляции» вам незнаком, то сначала вам нужно прочитать эту ссылку:

Isolation Level

Короче говоря, сериализуемый уровень изоляции обычно означает, что после того, как вы ВЫБРАЛИ ряд строк в транзакции, вы будете получать идентичные данные обратно каждый раз, когда вы повторно выпускаете этот SELECT. Если вы находитесь на следующем, более низком уровне изоляции, «повторяемое чтение», вы увидите вновь добавленные строки (и больше не увидите удаленные строки), но для строк, которые вы уже загрузили, вы не увидите никаких изменений. Только если вы находитесь на более низком уровне изоляции, например, «чтение с фиксацией», становится возможным увидеть, как строка данных меняет свое значение.

Информацию об управлении уровнем изоляции при использовании SQLAlchemy ORM см. в разделе Настройка уровней изоляции транзакций / DBAPI AUTOCOMMIT.

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

Чтобы понять, что мы подразумеваем под «транзакцией», когда говорим о Session, ваш Session предназначен для работы только в рамках транзакции. Обзор этого приведен в Управление транзакциями.

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

Три способа, от наиболее распространенного к наименее распространенному:

  1. Мы просто завершаем нашу транзакцию и начинаем новую при следующем доступе с нашим Session, вызывая Session.commit() (обратите внимание, что если Session находится в менее используемом режиме «autocommit», то также будет вызов Session.begin()). Подавляющее большинство приложений и сценариев использования не имеют проблем с невозможностью «видеть» данные в других транзакциях, потому что они придерживаются этого шаблона, который лежит в основе лучшей практики короткоживущих транзакций. См. Когда я строю Session, когда я фиксирую его и когда я закрываю его? для некоторых мыслей по этому поводу.

  2. Мы говорим нашему Session повторно прочитать строки, которые он уже прочитал, либо при следующем запросе на них с помощью Session.expire_all() или Session.expire(), либо сразу на объекте с помощью refresh. Подробнее об этом см. в Обновление / истечение срока действия.

  3. Мы можем запускать целые запросы, задавая для них определенную перезапись уже загруженных объектов по мере чтения строк, используя «populate existing». Это вариант выполнения, описанный в Заполнить существующие.

Но помните, ** ORM не сможет увидеть изменения в строках, если наш уровень изоляции - повторяющееся чтение или выше, если мы не начнем новую транзакцию**.

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

Эта ошибка возникает, когда Session.flush() вызывает исключение, откатывает транзакцию, но дальнейшие команды на Session вызываются без явного вызова Session.rollback() или Session.close().

Обычно это соответствует приложению, которое ловит исключение при Session.flush() или Session.commit() и не обрабатывает его должным образом. Например:

from sqlalchemy import create_engine, Column, Integer
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base(create_engine("sqlite://"))


class Foo(Base):
    __tablename__ = "foo"
    id = Column(Integer, primary_key=True)


Base.metadata.create_all()

session = sessionmaker()()

# constraint violation
session.add_all([Foo(id=1), Foo(id=1)])

try:
    session.commit()
except:
    # ignore error
    pass

# continue using session without rolling back
session.commit()

Использование Session должно вписываться в структуру, подобную этой:

try:
    <use session>
    session.commit()
except:
   session.rollback()
   raise
finally:
   session.close()  # optional, depends on use case

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

Это не означает, что блоки try/except должны присутствовать во всем приложении, что не является масштабируемой архитектурой. Вместо этого, типичный подход заключается в том, что при первом вызове методов и функций, ориентированных на ORM, процесс, вызывающий функции с самого верха, находится в блоке, который фиксирует транзакции при успешном завершении серии операций, а также откатывает транзакции назад, если операции не удались по какой-либо причине, включая неудачный слив. Существуют также подходы, использующие декораторы функций или менеджеры контекста для достижения аналогичных результатов. Выбор подхода во многом зависит от типа приложения, которое пишется.

Подробное обсуждение того, как организовать использование Session, смотрите в разделе Когда я строю Session, когда я фиксирую его и когда я закрываю его?.

Но почему flush() настаивает на выдаче ROLLBACK?

Было бы замечательно, если бы Session.flush() мог частично завершить работу и затем не откатываться назад, однако это выходит за рамки его текущих возможностей, поскольку его внутренний бухгалтерский учет должен быть изменен таким образом, чтобы он мог быть остановлен в любое время и точно соответствовать тому, что было сброшено в базу данных. Хотя теоретически это возможно, полезность такого усовершенствования значительно снижается из-за того, что многие операции с базой данных требуют ROLLBACK в любом случае. В частности, в Postgres есть операции, после выполнения которых транзакция не может быть продолжена:

test=> create table foo(id integer primary key);
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "foo_pkey" for table "foo"
CREATE TABLE
test=> begin;
BEGIN
test=> insert into foo values(1);
INSERT 0 1
test=> commit;
COMMIT
test=> begin;
BEGIN
test=> insert into foo values(1);
ERROR:  duplicate key value violates unique constraint "foo_pkey"
test=> insert into foo values(2);
ERROR:  current transaction is aborted, commands ignored until end of transaction block

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

Но почему одного автоматического призыва ROLLBACK недостаточно? Почему я должен снова отступать?

Откат, вызванный flush(), не является завершением полного блока транзакции; хотя он завершает транзакцию базы данных, с точки зрения Session все еще существует транзакция, которая сейчас находится в неактивном состоянии.

Дается такой блок, как:

sess = Session()  # begins a logical transaction
try:
    sess.flush()

    sess.commit()
except:
    sess.rollback()

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

Когда приведенный выше блок flush() не срабатывает, код все еще находится внутри транзакции, обрамленной блоком try/commit/except/rollback. Если flush() полностью откатит логическую транзакцию, это будет означать, что когда мы затем дойдем до блока except:, блок Session будет в чистом состоянии, готовый выдать новый SQL в новой транзакции, а вызов Session.rollback() будет вне последовательности. В частности, Session к этому моменту уже начал новую транзакцию, которую Session.rollback() будет выполнять ошибочно. Вместо того чтобы позволить операциям SQL продолжить новую транзакцию в этом месте, где, согласно нормальной практике, должен произойти откат, Session вместо этого отказывается продолжать работу до тех пор, пока явный откат действительно не произойдет.

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

Как сделать запрос, который всегда добавляет определенный фильтр к каждому запросу?

См. рецепт на сайте FilteredQuery.

Мой запрос не возвращает то же количество объектов, которое сообщает мне query.count() - почему?

Объект Query, когда его просят вернуть список объектов, сопоставленных с ORM, будет дублировать объекты на основе первичного ключа. То есть, если мы, например, используем отображение User, описанное в Объектно-реляционный учебник (API 1.x), и у нас есть SQL-запрос, подобный следующему:

q = session.query(User).outerjoin(User.addresses).filter(User.name == "jack")

В примере данных, используемом в учебнике, в таблице addresses есть две строки для строки users с именем 'jack', значение первичного ключа 5. Если мы зададим вышеприведенный запрос для Query.count(), то получим ответ 2:

>>> q.count()
2

Однако, если мы выполним Query.all() или итерацию запроса, мы получим один элемент:

>>> q.all()
[User(id=5, name='jack', ...)]

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

>>> session.query(User.id, User.name).outerjoin(User.addresses).filter(
...     User.name == "jack"
... ).all()
[(5, 'jack'), (5, 'jack')]

Есть две основные причины, по которым Query будет дедуплицировать:

  • Для обеспечения корректной работы объединенной ускоренной загрузки - Присоединился к Eager Loading работает путем запроса строк с использованием объединений по связанным таблицам, где он затем направляет строки из этих объединений в коллекции по ведущим объектам. Чтобы сделать это, он должен получить строки, в которых первичный ключ ведущего объекта повторяется для каждого вложенного элемента. Эта схема может быть продолжена в дальнейшие подколлекции, так что для одного ведущего объекта может быть обработано несколько строк, например User(id=5). Дедупликация позволяет нам получать объекты в том виде, в котором они были запрошены, например, все объекты User(), имя которых 'jack', что для нас является одним объектом, при этом коллекция User.addresses загружается с нетерпением, как было указано lazy='joined' на relationship() или через опцию joinedload(). Для согласованности, дедупликация все еще применяется независимо от того, установлена ли объединенная загрузка или нет, так как основная философия нетерпеливой загрузки заключается в том, что эти опции никогда не влияют на результат.

  • Для устранения путаницы в отношении карты идентичности - по общему признанию, это менее важная причина. Поскольку Session использует identity map, даже если наш набор результатов SQL содержит две строки с первичным ключом 5, существует только один объект User(id=5) внутри Session, который должен поддерживаться уникальным по своей идентичности, то есть по комбинации первичный ключ/класс. На самом деле не имеет особого смысла при запросе объектов User() получать один и тот же объект несколько раз в списке. Упорядоченное множество потенциально было бы лучшим представлением того, что Query стремится вернуть, когда возвращает полные объекты.

Вопрос дедупликации Query остается проблематичным, в основном по той единственной причине, что метод Query.count() является непоследовательным, а текущее состояние дел таково, что объединенная ускоренная загрузка в последних выпусках была вытеснена сначала стратегией «ускоренная загрузка подзапроса», а затем стратегией «ускоренная загрузка select IN», обе из которых в целом больше подходят для ускоренной загрузки коллекции. По мере дальнейшего развития SQLAlchemy может изменить это поведение на Query, что также может повлечь за собой появление новых API для более прямого управления этим поведением, а также может изменить поведение объединенной ускоренной загрузки для создания более последовательной модели использования.

Я создал связку с Outer Join, и хотя запрос возвращает строки, никакие объекты не возвращаются. Почему?

Строки, возвращаемые внешним объединением, могут содержать NULL для части первичного ключа, поскольку первичный ключ является составным для обеих таблиц. Объект Query игнорирует входящие строки, которые не имеют допустимого первичного ключа. Исходя из установки флага allow_partial_pks на mapper(), первичный ключ принимается, если у него есть хотя бы одно не-NULL значение, или, альтернативно, если у него нет NULL значений. См. allow_partial_pks в mapper().

Я использую joinedload() или lazy=False для создания JOIN/OUTER JOIN, и SQLAlchemy не создает правильный запрос, когда я пытаюсь добавить WHERE, ORDER BY, LIMIT и т.д. (который зависит от (OUTER) JOIN).

Соединения, создаваемые при нетерпеливой загрузке, используются только для полной загрузки связанных коллекций и не оказывают влияния на первичные результаты запроса. Поскольку они являются анонимными псевдонимами, на них нельзя ссылаться напрямую.

Подробнее об этом поведении см. в разделе Дзен присоединенной загрузки.

В запросе нет __len__(), почему?

Магический метод Python __len__(), примененный к объекту, позволяет использовать встроенный метод len() для определения длины коллекции. Интуитивно понятно, что объект SQL-запроса мог бы связать __len__() с методом Query.count(), который выдает SELECT COUNT. Причина, по которой это невозможно, заключается в том, что оценка запроса в виде списка повлечет за собой два вызова SQL вместо одного:

class Iterates(object):
    def __len__(self):
        print("LEN!")
        return 5

    def __iter__(self):
        print("ITER!")
        return iter([1, 2, 3, 4, 5])


list(Iterates())

выход:

ITER!
LEN!

Как использовать текстовый SQL с запросами ORM?

См:

Я вызываю Session.delete(myobject) и он не удаляется из родительской коллекции!

Описание этого поведения см. в Примечания по удалению - Удаление объектов, на которые ссылаются коллекции и скалярные отношения.

почему мой __init__() не вызывается при загрузке объектов?

Описание этого поведения см. в Конструкторы и инициализация объектов.

как использовать ON DELETE CASCADE с ORM SA?

SQLAlchemy всегда будет выдавать операторы UPDATE или DELETE для зависимых строк, которые в настоящее время загружены в Session. Для строк, которые не загружены, он по умолчанию будет выдавать операторы SELECT для загрузки этих строк и их обновления/удаления; другими словами, он предполагает, что нет настроенного ON DELETE CASCADE. Чтобы настроить SQLAlchemy на сотрудничество с ON DELETE CASCADE, смотрите Использование каскада внешних ключей ON DELETE с отношениями ORM.

Я установил атрибут «foo_id» для моего экземпляра в «7», но атрибут «foo» все еще None - разве он не должен был загрузить Foo с id #7?

ORM не построен таким образом, чтобы поддерживать немедленное создание отношений, вызванных изменениями атрибутов внешних ключей - вместо этого он спроектирован для работы наоборот - атрибуты внешних ключей обрабатываются ORM за кулисами, а конечный пользователь устанавливает объектные отношения естественным образом. Поэтому рекомендуемый способ установить o.foo - это сделать именно это - установить его!:

foo = Session.query(Foo).get(7)
o.foo = foo
Session.commit()

Манипуляции с атрибутами внешних ключей, конечно, полностью законны. Однако, установка атрибута внешнего ключа в новое значение в настоящее время не вызывает события «истечения срока действия» relationship(), в котором он задействован. Это означает, что для следующей последовательности:

o = Session.query(SomeClass).first()
assert o.foo is None  # accessing an un-set attribute sets it to None
o.foo_id = 7

o.foo инициализируется в None, когда мы впервые обратились к нему. Установка o.foo_id = 7 будет иметь значение «7» как ожидающее, но смыва не произошло - поэтому o.foo по-прежнему None:

# attribute is already set to None, has not been
# reconciled with o.foo_id = 7 yet
assert o.foo is None

Для загрузки o.foo на основе внешнего ключа мутация обычно происходит естественным образом после фиксации, которая одновременно сбрасывает новое значение внешнего ключа и уничтожает все состояние:

Session.commit()  # expires all attributes

foo_7 = Session.query(Foo).get(7)

assert o.foo is foo_7  # o.foo lazyloads on access

Более минимальной операцией является истечение срока действия атрибута по отдельности - это можно сделать для любого объекта persistent с помощью Session.expire():

o = Session.query(SomeClass).first()
o.foo_id = 7
Session.expire(o, ["foo"])  # object must be persistent for this

foo_7 = Session.query(Foo).get(7)

assert o.foo is foo_7  # o.foo lazyloads on access

Обратите внимание, что если объект не является постоянным, но присутствует в Session, то он известен как pending. Это означает, что строка для объекта еще не была INSERTed в базу данных. Для такого объекта установка foo_id не имеет смысла, пока ряд не будет вставлен; в противном случае ряда еще нет:

new_obj = SomeClass()
new_obj.foo_id = 7

Session.add(new_obj)

# accessing an un-set attribute sets it to None
assert new_obj.foo is None

Session.flush()  # emits INSERT

# expire this because we already set .foo to None
Session.expire(o, ["foo"])

assert new_obj.foo is foo_7  # now it loads

В рецепте ExpireRelationshipOnFKChange приведен пример использования событий SQLAlchemy для координации установки атрибутов внешнего ключа в отношениях «многие-к-одному».

Есть ли способ автоматически иметь только уникальные ключевые слова (или другие виды объектов) без выполнения запроса на ключевое слово и получения ссылки на строку, содержащую это ключевое слово?

Когда люди читают пример many-to-many в документации, они сталкиваются с тем, что если вы создаете один и тот же Keyword дважды, он попадает в БД дважды. Что несколько неудобно.

Этот рецепт UniqueObject был создан для решения этой проблемы.

Почему post_update выдает UPDATE в дополнение к первому UPDATE?

Функция post_update, задокументированная в Строки, указывающие сами на себя / Взаимозависимые строки, подразумевает, что в ответ на изменения определенного связанного отношениями внешнего ключа выдается оператор UPDATE в дополнение к INSERT/UPDATE/DELETE, которые обычно выдаются для целевого ряда. Хотя основной целью этого оператора UPDATE является то, что он работает в паре с INSERT или DELETE этого ряда, так что он может пост-устанавливать или пред-устанавливать ссылку на внешний ключ, чтобы разорвать цикл с взаимозависимым внешним ключом, в настоящее время он также объединен как второй UPDATE, который испускается, когда целевой ряд сам подвергается UPDATE. В этом случае UPDATE, испускаемый post_update, обычно не нужен и часто выглядит расточительным.

Однако некоторые исследования в попытке устранить это поведение «UPDATE / UPDATE» показывают, что для этого необходимо внести значительные изменения в процесс единицы работы не только в реализации post_update, но и в областях, не связанных с post_update, поскольку в некоторых случаях порядок операций должен быть изменен на стороне, не связанной с post_update, что в свою очередь может повлиять на другие случаи, такие как правильная обработка UPDATE значения первичного ключа со ссылкой (см. #1063 для доказательства концепции).

Ответ заключается в том, что «post_update» используется для разрыва цикла между двумя взаимозависимыми внешними ключами, и если этот цикл будет ограничен только INSERT/DELETE целевой таблицы, это означает, что упорядочивание операторов UPDATE в других местах должно быть либерализовано, что приведет к разрыву в других крайних случаях.

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