Государственное управление

Краткое введение в состояния объектов

Полезно знать состояния, в которых может находиться экземпляр в течение сессии:

  • Transient - экземпляр, который не находится в сессии и не сохраняется в базе данных; т.е. он не имеет идентификатора базы данных. Единственная связь такого объекта с ORM заключается в том, что его класс имеет Mapper, связанный с ним.

  • Pending - когда вы Session.add() переходящий экземпляр, он становится ожидающим. На самом деле он еще не был сброшен в базу данных, но будет сброшен при следующем сбросе.

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

  • Deleted - Экземпляр, который был удален в рамках flush, но транзакция еще не завершилась. Объекты в этом состоянии по сути противоположны состоянию «ожидание»; когда транзакция сессии будет зафиксирована, объект перейдет в состояние отсоединения. В качестве альтернативы, когда транзакция сессии откатывается, удаленный объект переходит обратно в состояние persistent.

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

Для более глубокого погружения во все возможные переходы состояний смотрите раздел События жизненного цикла объекта, в котором описывается каждый переход, а также то, как программно отслеживать каждый из них.

Получение текущего состояния объекта

Фактическое состояние любого сопоставленного объекта можно посмотреть в любое время с помощью функции inspect() на сопоставленном экземпляре; эта функция вернет соответствующий объект InstanceState, который управляет внутренним состоянием ORM для объекта. InstanceState предоставляет, среди прочих аксессоров, булевы атрибуты, указывающие на состояние персистентности объекта, включая:

Например:

>>> from sqlalchemy import inspect
>>> insp = inspect(my_object)
>>> insp.persistent
True

См.также

Проверка сопоставленных экземпляров - дальнейшие примеры InstanceState

Атрибуты сессии

Сам Session действует в некотором роде как коллекция, подобная множеству. Доступ ко всем присутствующим элементам можно получить с помощью интерфейса итератора:

for obj in session:
    print(obj)

А наличие можно проверить, используя обычную семантику «содержит»:

if obj in session:
    print("Object is present")

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

# pending objects recently added to the Session
session.new

# persistent objects which currently have changes detected
# (this collection is now created on the fly each time the property is called)
session.dirty

# persistent objects that have been marked as deleted via session.delete(obj)
session.deleted

# dictionary of all persistent objects, keyed on their
# identity key
session.identity_map

(Документация: Session.new, Session.dirty, Session.deleted, Session.identity_map).

Поведение при обращении к сеансам

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

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

Также возможен подход, основанный на событиях. Простой рецепт, обеспечивающий поведение «сильной ссылки» для всех объектов, пока они остаются в состоянии persistent, выглядит следующим образом:

from sqlalchemy import event


def strong_reference_session(session):
    @event.listens_for(session, "pending_to_persistent")
    @event.listens_for(session, "deleted_to_persistent")
    @event.listens_for(session, "detached_to_persistent")
    @event.listens_for(session, "loaded_as_persistent")
    def strong_ref_object(sess, instance):
        if "refs" not in sess.info:
            sess.info["refs"] = refs = set()
        else:
            refs = sess.info["refs"]

        refs.add(instance)

    @event.listens_for(session, "persistent_to_detached")
    @event.listens_for(session, "persistent_to_deleted")
    @event.listens_for(session, "persistent_to_transient")
    def deref_object(sess, instance):
        sess.info["refs"].discard(instance)

Выше мы перехватили крючки событий SessionEvents.pending_to_persistent(), SessionEvents.detached_to_persistent(), SessionEvents.deleted_to_persistent() и SessionEvents.loaded_as_persistent(), чтобы перехватывать объекты, когда они входят в переход persistent, и крючки SessionEvents.persistent_to_detached() и SessionEvents.persistent_to_deleted(), чтобы перехватывать объекты, когда они выходят из постоянного состояния.

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

from sqlalchemy.orm import Session

my_session = Session()
strong_reference_session(my_session)

Он также может быть вызван для любого sessionmaker:

from sqlalchemy.orm import sessionmaker

maker = sessionmaker()
strong_reference_session(maker)

Объединение

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

merged_object = session.merge(existing_object)

Когда ему дается экземпляр, он выполняет следующие действия:

  • Он проверяет первичный ключ экземпляра. Если он присутствует, он пытается найти этот экземпляр в локальной карте идентификации. Если флаг load=True оставлен по умолчанию, он также проверяет базу данных на наличие этого первичного ключа, если он не находится локально.

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

  • Затем состояние данного экземпляра копируется на расположенный/новый созданный экземпляр. Для значений атрибутов, присутствующих на исходном экземпляре, значение переносится на целевой экземпляр. Для значений атрибутов, которые отсутствуют на исходном экземпляре, соответствующий атрибут на целевом экземпляре удаляется из памяти expired, что приводит к удалению любого локально присутствующего значения целевого экземпляра для этого атрибута, но не происходит прямой модификации хранящегося в базе данных значения для этого атрибута.

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

  • Операция каскадируется на связанные объекты и коллекции, на что указывает каскад merge (см. Каскады).

  • Возвращается новый экземпляр.

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

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

  • Приложение хранит объекты в кэше в памяти, разделяемом одновременно многими Session объектами. Session.merge() используется каждый раз, когда объект извлекается из кэша для создания его локальной копии в каждом Session, который его запрашивает. Кэшированный объект остается отделенным; только его состояние перемещается в копии самого себя, локальные для отдельных объектов Session.

    В случае использования кэширования обычно используется флаг load=False, чтобы снять накладные расходы на согласование состояния объекта с базой данных. Существует также «объемная» версия Session.merge() под названием Query.merge_result(), которая была разработана для работы с расширенными кэшем объектами Query - см. раздел Кэширование Dogpile.

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

    В случае использования «передачи между потоками/процессами» приложение может захотеть использовать флаг load=False, чтобы избежать накладных расходов и избыточных SQL-запросов при передаче данных.

Советы по слиянию

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

Используем канонический пример объектов User и Address:

class User(Base):
    __tablename__ = "user"

    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50), nullable=False)
    addresses = relationship("Address", backref="user")


class Address(Base):
    __tablename__ = "address"

    id = mapped_column(Integer, primary_key=True)
    email_address = mapped_column(String(50), nullable=False)
    user_id = mapped_column(Integer, ForeignKey("user.id"), nullable=False)

Предположим, что объект User с одним Address, уже персистентный:

>>> u1 = User(name="ed", addresses=[Address(email_address="ed@ed.com")])
>>> session.add(u1)
>>> session.commit()

Теперь мы создаем a1, объект вне сессии, который мы хотим объединить поверх существующего Address:

>>> existing_a1 = u1.addresses[0]
>>> a1 = Address(id=existing_a1.id)

Будет неожиданностью, если мы скажем следующее:

>>> a1.user = u1
>>> a1 = session.merge(a1)
>>> session.commit()
sqlalchemy.orm.exc.FlushError: New instance <Address at 0x1298f50>
with identity key (<class '__main__.Address'>, (1,)) conflicts with
persistent instance <Address at 0x12a25d0>

Почему? Мы не были осторожны с нашими каскадами. Присвоение a1.user постоянному объекту каскадировало на обратную ссылку User.addresses и сделало наш объект a1 отложенным, как будто мы его добавили. Теперь у нас есть два объекта Address в сессии:

>>> a1 = Address()
>>> a1.user = u1
>>> a1 in session
True
>>> existing_a1 in session
True
>>> a1 is existing_a1
False

Выше, наша a1 уже ожидает выполнения в сессии. Последующая операция Session.merge() по сути ничего не делает. Каскад можно настроить с помощью опции relationship.cascade на relationship(), хотя в данном случае это означало бы удаление каскада save-update из отношения User.addresses - а обычно такое поведение чрезвычайно удобно. Решением здесь обычно будет не присваивать a1.user объекту, уже персистентному в целевой сессии.

Опция cascade_backrefs=False для relationship() также предотвратит добавление Address к сессии через назначение a1.user = u1.

Более подробная информация о работе каскада приведена в Каскады.

Еще один пример неожиданного состояния:

>>> a1 = Address(id=existing_a1.id, user_id=u1.id)
>>> a1.user = None
>>> a1 = session.merge(a1)
>>> session.commit()
sqlalchemy.exc.IntegrityError: (IntegrityError) address.user_id
may not be NULL

Выше назначение user имеет приоритет над назначением внешнего ключа user_id, в результате чего None применяется к user_id, вызывая сбой.

Большинство вопросов Session.merge() можно рассмотреть, сначала проверив - находится ли объект преждевременно в сессии?

>>> a1 = Address(id=existing_a1, user_id=user.id)
>>> assert a1 not in session
>>> a1 = session.merge(a1)

Или на объекте есть состояние, которое нам не нужно? Изучение __dict__ является быстрым способом проверки:

>>> a1 = Address(id=existing_a1, user_id=user.id)
>>> a1.user
>>> a1.__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x1298d10>,
    'user_id': 1,
    'id': 1,
    'user': None}
>>> # we don't want user=None merged, remove it
>>> del a1.user
>>> a1 = session.merge(a1)
>>> # success
>>> session.commit()

Expunging

Expunge удаляет объект из сессии, отправляя постоянные экземпляры в состояние detached, а отложенные экземпляры - в состояние transient:

session.expunge(obj1)

Чтобы удалить все элементы, вызовите Session.expunge_all() (ранее этот метод был известен как clear()).

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

Expiring означает, что хранящиеся в базе данных данные, содержащиеся в серии атрибутов объекта, стираются таким образом, что при следующем обращении к этим атрибутам будет выдан SQL-запрос, который обновит эти данные из базы данных.

Когда мы говорим об истечении срока действия данных, мы обычно имеем в виду объект, находящийся в состоянии persistent. Например, если мы загрузим объект следующим образом:

user = session.scalars(select(User).filter_by(name="user1").limit(1)).first()

Приведенный выше объект User является постоянным и имеет ряд атрибутов; если бы мы заглянули внутрь его __dict__, то увидели бы, что состояние загружено:

>>> user.__dict__
{
  'id': 1, 'name': u'user1',
  '_sa_instance_state': <...>,
}

где id и name относятся к этим столбцам в базе данных. _sa_instance_state - это значение, не относящееся к базе данных, используемое SQLAlchemy внутренне (оно ссылается на InstanceState для экземпляра. Хотя это не имеет прямого отношения к данному разделу, если мы хотим получить доступ к нему, мы должны использовать функцию inspect() для доступа к нему).

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

>>> session.expire(user)
>>> user.__dict__
{'_sa_instance_state': <...>}

Мы видим, что в то время как внутреннее «состояние» все еще сохраняется, значения, соответствующие столбцам id и name, исчезли. Если бы мы обратились к одному из этих столбцов и наблюдали за SQL, мы бы увидели следующее:

>>> print(user.name)
{execsql}SELECT user.id AS user_id, user.name AS user_name
FROM user
WHERE user.id = ?
(1,)
{stop}user1

Выше, при обращении к истекшему атрибуту user.name, ORM инициировал lazy load для получения последнего состояния из базы данных, выдав SELECT для строки пользователя, к которой относится этот пользователь. После этого __dict__ снова заполняется:

>>> user.__dict__
{
  'id': 1, 'name': u'user1',
  '_sa_instance_state': <...>,
}

Примечание

Пока мы заглядываем внутрь __dict__, чтобы посмотреть, что SQLAlchemy делает с атрибутами объекта, мы **не должны изменять содержимое __dict__ напрямую, по крайней мере, в отношении тех атрибутов, которые поддерживает SQLAlchemy ORM (другие атрибуты, не относящиеся к сфере SQLA, вполне подходят). Это связано с тем, что SQLAlchemy использует descriptors для отслеживания изменений, которые мы вносим в объект, и когда мы изменяем __dict__ напрямую, ORM не сможет отследить, что мы что-то изменили.

Другим ключевым поведением как Session.expire(), так и Session.refresh() является то, что все изменения объекта, не прошедшие очистку, отбрасываются. То есть, если мы изменим атрибут нашего User:

>>> user.name = "user2"

но затем мы вызываем Session.expire() без предварительного вызова Session.flush(), наше ожидающее значение 'user2' отбрасывается:

>>> session.expire(user)
>>> user.name
'user1'

Метод Session.expire() может быть использован для пометки как «истекший» всех ORM-сопоставленных атрибутов для экземпляра:

# expire all ORM-mapped attributes on obj1
session.expire(obj1)

ему также может быть передан список строковых имен атрибутов, ссылающихся на конкретные атрибуты, которые должны быть помечены как истекшие:

# expire only attributes obj1.attr1, obj1.attr2
session.expire(obj1, ["attr1", "attr2"])

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

session.expire_all()

Метод Session.refresh() имеет аналогичный интерфейс, но вместо истечения срока действия он выдает немедленный SELECT для строки объекта сразу:

# reload all attributes on obj1
session.refresh(obj1)

Session.refresh() также принимает список строковых имен атрибутов, но, в отличие от Session.expire(), ожидает, что хотя бы одно имя будет именем атрибута, отображенного на столбец:

# reload obj1.attr1, obj1.attr2
session.refresh(obj1, ["attr1", "attr2"])

Совет

Альтернативным методом обновления, который часто является более гибким, является использование функции Заполнить существующие ORM, доступной для запросов 2.0 style с select(), а также из метода Query.populate_existing() Query внутри запросов 1.x style. При использовании этого варианта выполнения все объекты ORM, возвращаемые в наборе результатов запроса, будут обновлены данными из базы данных:

stmt = (
    select(User)
    .execution_options(populate_existing=True)
    .where((User.name.in_(["a", "b", "c"])))
)
for user in session.execute(stmt).scalars():
    print(user)  # will be refreshed for those columns that came back from the query

Более подробную информацию см. в разделе Заполнить существующие.

Что на самом деле загружается

Оператор SELECT, который выдается, когда объект помечен Session.expire() или загружен Session.refresh(), зависит от нескольких факторов, включая:

  • Загрузка атрибутов с истекшим сроком действия выполняется только для атрибутов, сопоставленных с колонками. Хотя любой тип атрибута может быть помечен как просроченный, включая relationship() - сопоставленный атрибут, обращение к просроченному атрибуту relationship() вызовет загрузку только для этого атрибута, используя стандартную ленивую загрузку, ориентированную на отношения. Атрибуты, ориентированные на столбцы, даже если они просрочены, не будут загружаться в рамках этой операции, а вместо этого будут загружаться при обращении к любому атрибуту, ориентированному на столбцы.

  • relationship()- сопоставленные атрибуты не будут загружаться в ответ на обращение к атрибутам на основе столбцов с истекшим сроком действия.

  • Что касается отношений, Session.refresh() является более ограничительным, чем Session.expire() в отношении атрибутов, которые не отображены на столбцы. Вызов Session.refresh() и передача списка имен, включающего только атрибуты, сопоставленные с отношениями, приведет к ошибке. В любом случае, атрибуты relationship(), не загружаемые с нетерпением, не будут включены ни в одну операцию обновления.

  • Атрибуты relationship(), настроенные на «нетерпеливую загрузку» через параметр relationship.lazy, будут загружаться в случае Session.refresh(), если либо не указаны имена атрибутов, либо если их имена включены в список обновляемых атрибутов.

  • Атрибуты, настроенные как deferred(), обычно не загружаются ни во время загрузки атрибута с истекшим сроком действия, ни во время обновления. Невыгруженный атрибут, имеющий значение deferred(), загружается сам по себе при прямом обращении к нему, или если он входит в «группу» отложенных атрибутов, когда происходит обращение к невыгруженному атрибуту в этой группе.

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

  • Когда Session.refresh() используется в связке таблиц наследования, выдаваемый SELECT будет похож на тот, который используется в случае использования Session.query() в классе целевого объекта. Обычно это все те таблицы, которые устанавливаются как часть отображения.

Когда истекает или обновляется

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

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

  • некоторый SQL был выдан в транзакции вне рамок обработки объектов ORM, например, если конструкция Table.update() была выдана с помощью метода Session.execute();

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

Во втором пункте есть важная оговорка: «Также известно, что действующие правила изоляции позволяют этим данным быть видимыми». Это означает, что нельзя предполагать, что UPDATE, произошедший на другом соединении базы данных, будет виден здесь локально; во многих случаях это не так. Вот почему, если вы хотите использовать Session.expire() или Session.refresh() для просмотра данных между текущими транзакциями, понимание действующих правил изоляции является необходимым.

См.также

Session.expire()

Session.expire_all()

Session.refresh()

Заполнить существующие - позволяет любому ORM-запросу обновлять объекты так, как они загружаются обычно, обновляя все совпадающие объекты в карте идентификации по результатам оператора SELECT.

isolation - глоссарное объяснение изоляции, включающее ссылки на Википедию.

The SQLAlchemy Session In-Depth - видео + слайды с углубленным обсуждением жизненного цикла объекта, включая роль истечения срока действия данных.

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