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

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

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

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

Isolation Level

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

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

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

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

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

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

  1. Мы просто завершаем нашу транзакцию и начинаем новую при следующем обращении к ней с помощью нашего Session, вызывая Session.commit() (обратите внимание, что если Session находится в менее распространенном режиме «автокоммита», то также будет вызван 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, описанное в Использование декларативных форм ORM для определения метаданных таблицы, и у нас есть SQL-запрос следующего вида:

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

В примере данных, используемом в учебном пособии, в таблице addresses для строки с именем 'jack', значением первичного ключа 5 в таблице users имеется две строки. Если мы зададим приведенный выше запрос для строки 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, внутри Session есть только один объект User(id=5), который должен храниться уникально по своей идентичности, т.е. по комбинации первичный ключ/класс. На самом деле не имеет особого смысла при запросе объектов 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:
    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 с идентификатором #7?

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

foo = session.get(Foo, 7)
o.foo = foo
Session.commit()

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

o = session.scalars(select(SomeClass).limit(1)).first()

# assume the existing o.foo_id value is None;
# accessing o.foo will reconcile this as ``None``, but will effectively
# "load" the value of None
assert o.foo is None

# now set foo_id to something.  o.foo will not be immediately affected
o.foo_id = 7

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

# attribute is already "loaded" as 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.get(Foo, 7)

# o.foo will lazyload again, this time getting the new object
assert o.foo is foo_7

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

o = session.scalars(select(SomeClass).limit(1)).first()
o.foo_id = 7
Session.expire(o, ["foo"])  # object must be persistent for this

foo_7 = session.get(Foo, 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)

# returns None but this is not a "lazyload", as the object is not
# persistent in the DB yet, and the None value is not part of the
# object's state
assert new_obj.foo is None

Session.flush()  # emits INSERT

assert new_obj.foo is foo_7  # now it loads

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

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

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

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

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

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

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

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

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