Каскады¶
Картографы поддерживают концепцию конфигурируемого поведения cascade на конструкциях relationship()
. Это относится к тому, как операции, выполняемые над «родительским» объектом относительно определенного Session
, должны распространяться на элементы, на которые ссылается это отношение (например, «дочерние» объекты), и на это влияет опция relationship.cascade
.
Поведение cascade по умолчанию ограничено каскадами так называемых настроек save-update и объединить. Типичной «альтернативной» настройкой для каскада является добавление опций удалить и delete-orphan; эти настройки подходят для связанных объектов, которые существуют только до тех пор, пока они присоединены к своему родителю, а в противном случае удаляются.
Каскадное поведение настраивается с помощью опции relationship.cascade
на relationship()
:
class Order(Base):
__tablename__ = "order"
items = relationship("Item", cascade="all, delete-orphan")
customer = relationship("User", cascade="save-update")
Чтобы установить каскады на обратную ссылку, тот же флаг можно использовать с функцией backref()
, которая в конечном итоге передает свои аргументы обратно в relationship()
:
class Item(Base):
__tablename__ = "item"
order = relationship(
"Order", backref=backref("items", cascade="all, delete-orphan")
)
По умолчанию значение relationship.cascade
равно save-update, merge
. Типичным альтернативным значением этого параметра является all
или, чаще всего, all, delete-orphan
. Символ all
является синонимом save-update, merge, refresh-expire, expunge, delete
, и его использование в сочетании с delete-orphan
означает, что дочерний объект должен следовать вместе со своим родителем во всех случаях и удаляться, когда он больше не связан с этим родителем.
Предупреждение
Опция all
cascade подразумевает настройку refresh-expire cascade, что может быть нежелательно при использовании расширения Асинхронный ввод/вывод (asyncio), так как это приведет к более агрессивному истечению срока действия связанных объектов, чем обычно уместно в явном контексте IO. Дополнительную информацию см. в примечаниях Предотвращение неявного ввода-вывода при использовании AsyncSession.
Список доступных значений, которые можно указать для параметра relationship.cascade
, описан в следующих подразделах.
save-update¶
Каскад save-update
указывает, что когда объект помещается в Session
через Session.add()
, все объекты, связанные с ним через этот relationship()
, также должны быть добавлены в этот же Session
. Предположим, у нас есть объект user1
с двумя связанными с ним объектами address1
, address2
:
>>> user1 = User()
>>> address1, address2 = Address(), Address()
>>> user1.addresses = [address1, address2]
Если мы добавим user1
к Session
, он также добавит address1
, address2
неявно:
>>> sess = Session()
>>> sess.add(user1)
>>> address1 in sess
True
Каскад save-update
также влияет на операции с атрибутами для объектов, которые уже присутствуют в Session
. Если мы добавляем третий объект address3
в коллекцию user1.addresses
, он становится частью состояния этой коллекции Session
:
>>> address3 = Address()
>>> user1.addresses.append(address3)
>>> address3 in sess
True
Каскад save-update
может демонстрировать неожиданное поведение при удалении элемента из коллекции или отсоединении объекта от скалярного атрибута. В некоторых случаях осиротевшие объекты все еще могут быть втянуты в Session
бывшего родителя; это делается для того, чтобы процесс flush мог обработать связанный объект соответствующим образом. Этот случай обычно возникает только в том случае, если объект удаляется из одного Session
и добавляется в другой:
>>> user1 = sess1.query(User).filter_by(id=1).first()
>>> address1 = user1.addresses[0]
>>> sess1.close() # user1, address1 no longer associated with sess1
>>> user1.addresses.remove(address1) # address1 no longer associated with user1
>>> sess2 = Session()
>>> sess2.add(user1) # ... but it still gets added to the new session,
>>> address1 in sess2 # because it's still "pending" for flush
True
Каскад save-update
включен по умолчанию и обычно воспринимается как должное; он упрощает код, позволяя одним вызовом Session.add()
регистрировать сразу всю структуру объектов внутри этого Session
. Хотя его можно отключить, обычно в этом нет необходимости.
Один случай, когда каскад save-update
иногда мешает, заключается в том, что он происходит в обоих направлениях для двунаправленных отношений, например, обратных ссылок, что означает, что ассоциация дочернего объекта с определенным родителем может иметь эффект того, что родительский объект неявно ассоциируется с Session
этого дочернего объекта; этот паттерн, а также то, как изменить его поведение с помощью флага relationship.cascade_backrefs
, обсуждается в разделе Управление каскадом на обратных ссылках.
удалить¶
Каскад delete
указывает, что когда «родительский» объект помечен на удаление, его связанные «дочерние» объекты также должны быть помечены на удаление. Если, например, у нас есть отношение User.addresses
с настроенным каскадом delete
:
class User(Base):
# ...
addresses = relationship("Address", cascade="all, delete")
Если использовать приведенное выше отображение, у нас есть объект User
и два связанных объекта Address
:
>>> user1 = sess.query(User).filter_by(id=1).first()
>>> address1, address2 = user1.addresses
Если мы пометим user1
для удаления, то после выполнения операции flush address1
и address2
также будут удалены:
>>> sess.delete(user1)
>>> sess.commit()
DELETE FROM address WHERE address.id = ?
((1,), (2,))
DELETE FROM user WHERE user.id = ?
(1,)
COMMIT
В качестве альтернативы, если наше отношение User.addresses
не имеет каскада delete
, поведение SQLAlchemy по умолчанию заключается в том, чтобы вместо этого де-ассоциировать address1
и address2
от user1
, установив их ссылку внешнего ключа на NULL
. Используя следующее отображение:
class User(Base):
# ...
addresses = relationship("Address")
При удалении родительского объекта User
строки в address
не удаляются, а деассоциируются:
>>> sess.delete(user1)
>>> sess.commit()
UPDATE address SET user_id=? WHERE address.id = ?
(None, 1)
UPDATE address SET user_id=? WHERE address.id = ?
(None, 2)
DELETE FROM user WHERE user.id = ?
(1,)
COMMIT
Каскад удалить на отношениях «один-ко-многим» часто комбинируется с каскадом delete-orphan, который будет выдавать DELETE для связанной строки, если «дочерний» объект будет деассоциирован от родительского. Комбинация каскада delete
и delete-orphan
охватывает обе ситуации, когда SQLAlchemy приходится решать между установкой столбца внешнего ключа в NULL и полным удалением строки.
Функция по умолчанию работает полностью независимо от настроенных в базе данных ограничений FOREIGN KEY
, которые сами могут настраивать поведение CASCADE
. Для более эффективной интеграции с такой конфигурацией следует использовать дополнительные директивы, описанные в Использование каскада внешних ключей ON DELETE с отношениями ORM.
См.также
Использование каскада внешних ключей ON DELETE с отношениями ORM
Использование каскада удаления с отношениями «многие-ко-многим
Использование каскада удаления с отношениями «многие-ко-многим¶
Опция cascade="all, delete"
одинаково хорошо работает с отношениями «многие-ко-многим», в которых используется relationship.secondary
для указания таблицы ассоциаций. Когда родительский объект удаляется и, следовательно, деассоциируется с родственными объектами, рабочий процесс обычно удаляет строки из таблицы ассоциаций, но оставляет связанные объекты нетронутыми. В сочетании с cascade="all, delete"
дополнительные операторы DELETE
будут выполняться для самих дочерних строк.
Следующий пример адаптирует пример Многие ко многим для иллюстрации установки cascade="all, delete"
на одной стороне ассоциации:
association_table = Table(
"association",
Base.metadata,
Column("left_id", Integer, ForeignKey("left.id")),
Column("right_id", Integer, ForeignKey("right.id")),
)
class Parent(Base):
__tablename__ = "left"
id = Column(Integer, primary_key=True)
children = relationship(
"Child",
secondary=association_table,
back_populates="parents",
cascade="all, delete",
)
class Child(Base):
__tablename__ = "right"
id = Column(Integer, primary_key=True)
parents = relationship(
"Parent",
secondary=association_table,
back_populates="children",
)
Выше, когда объект Parent
помечается для удаления с помощью Session.delete()
, процесс flush, как обычно, удалит связанные строки из таблицы association
, однако по правилам каскада он также удалит все связанные строки Child
.
Предупреждение
Если бы вышеуказанная настройка cascade="all, delete"
была настроена на both отношения, то действие каскада продолжало бы каскадировать все объекты Parent
и Child
, загружая каждую коллекцию children
и parents
, с которой встречались, и удаляя все, что связано. Обычно нежелательно, чтобы каскад «удаления» был настроен двунаправленно.
Использование каскада внешних ключей ON DELETE с отношениями ORM¶
Поведение каскада «удаления» SQLAlchemy пересекается с функцией ON DELETE
ограничения базы данных FOREIGN KEY
. SQLAlchemy позволяет конфигурировать это поведение на уровне схемы DDL с помощью конструкций ForeignKey
и ForeignKeyConstraint
; использование этих объектов в сочетании с метаданными Table
описано в ON UPDATE и ON DELETE.
Чтобы использовать каскады внешних ключей ON DELETE
в сочетании с relationship()
, важно отметить, что параметр relationship.cascade
должен быть настроен в соответствии с желаемым поведением «удалить» или «установить нуль» (используя каскад delete
или оставляя его опущенным), Таким образом, независимо от того, будет ли ORM или ограничения на уровне базы данных выполнять задачу фактического изменения данных в базе данных, ORM все равно сможет соответствующим образом отслеживать состояние локально присутствующих объектов, которые могут быть затронуты.
Существует дополнительный параметр relationship()
, который указывает, в какой степени ORM должен сам пытаться выполнять операции DELETE/UPDATE для связанных строк, а в какой степени он должен полагаться на то, что каскад ограничений FOREIGN KEY со стороны базы данных справится с этой задачей; это параметр relationship.passive_deletes
, и он принимает варианты False
(по умолчанию), True
и "all"
.
Наиболее типичным примером является пример, когда дочерние строки должны удаляться при удалении родительских строк, и что ON DELETE CASCADE
также настроено на соответствующее ограничение FOREIGN KEY
:
class Parent(Base):
__tablename__ = "parent"
id = Column(Integer, primary_key=True)
children = relationship(
"Child",
back_populates="parent",
cascade="all, delete",
passive_deletes=True,
)
class Child(Base):
__tablename__ = "child"
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey("parent.id", ondelete="CASCADE"))
parent = relationship("Parent", back_populates="children")
Поведение приведенной выше конфигурации при удалении родительской строки выглядит следующим образом:
Приложение вызывает
session.delete(my_parent)
, гдеmy_parent
является экземпляромParent
.Когда
Session
в следующий раз передает изменения в базу данных, все текущие загруженные элементы в коллекцииmy_parent.children
удаляются ORM, то есть для каждой записи выдается операторDELETE
.Если коллекция
my_parent.children
является выгруженной, то никаких утвержденийDELETE
не выдается. Если бы флагrelationship.passive_deletes
был не установлен для этой коллекцииrelationship()
, то было бы выдано утверждениеSELECT
для выгруженных объектовChild
.Затем выдается оператор
DELETE
для самой строкиmy_parent
.Настройка
ON DELETE CASCADE
на уровне базы данных гарантирует, что все строки вchild
, которые ссылаются на затронутую строку вparent
, также будут удалены.Экземпляр
Parent
, на который ссылаетсяmy_parent
, а также все экземплярыChild
, которые были связаны с этим объектом и были загружены (т.е. произошел шаг 2 выше), деассоциируются отSession
.
Примечание
Чтобы использовать «ON DELETE CASCADE», базовый движок базы данных должен поддерживать ограничения FOREIGN KEY
, и они должны быть принудительными:
Чтобы использовать «ON DELETE CASCADE», базовый движок базы данных должен поддерживать ограничения mysql_storage_engines, и они должны быть принудительными:
При использовании SQLite поддержка внешних ключей должна быть включена явным образом. Подробности см. в разделе sqlite_foreign_keys.
Использование внешнего ключа ON DELETE в отношениях «многие-ко-многим¶
Как описано в Использование каскада удаления с отношениями «многие-ко-многим, каскад «удаления» работает и для отношений «многие-ко-многим». Чтобы использовать внешние ключи ON DELETE CASCADE
в сочетании с отношениями «многие ко многим», в таблице ассоциаций настраиваются директивы FOREIGN KEY
. Эти директивы могут справиться с задачей автоматического удаления из таблицы ассоциаций, но не могут обеспечить автоматическое удаление самих связанных объектов.
В этом случае директива relationship.passive_deletes
может избавить нас от дополнительных операторов SELECT
во время операции удаления, но все еще остаются некоторые коллекции, которые ORM будет продолжать загружать, чтобы найти затронутые дочерние объекты и правильно их обработать.
Примечание
Гипотетическая оптимизация для этого может включать единственный оператор DELETE
против всех связанных с родителями строк таблицы ассоциаций сразу, а затем использовать RETURNING
для нахождения затронутых связанных дочерних строк, однако в настоящее время это не является частью реализации единицы работы ORM.
В этой конфигурации мы настраиваем ON DELETE CASCADE
на оба ограничения внешнего ключа таблицы ассоциации. Мы настраиваем cascade="all, delete"
на стороне отношения родитель->ребенок, а затем мы можем настроить passive_deletes=True
на другой стороне двунаправленного отношения, как показано ниже:
association_table = Table(
"association",
Base.metadata,
Column("left_id", Integer, ForeignKey("left.id", ondelete="CASCADE")),
Column("right_id", Integer, ForeignKey("right.id", ondelete="CASCADE")),
)
class Parent(Base):
__tablename__ = "left"
id = Column(Integer, primary_key=True)
children = relationship(
"Child",
secondary=association_table,
back_populates="parents",
cascade="all, delete",
)
class Child(Base):
__tablename__ = "right"
id = Column(Integer, primary_key=True)
parents = relationship(
"Parent",
secondary=association_table,
back_populates="children",
passive_deletes=True,
)
Используя приведенную выше конфигурацию, удаление объекта Parent
происходит следующим образом:
Объект
Parent
помечается для удаления с помощьюSession.delete()
.Когда происходит flush, если коллекция
Parent.children
не загружена, ORM сначала выпустит оператор SELECT, чтобы загрузить объектыChild
, соответствующиеParent.children
.Затем он будет выдавать утверждения
DELETE
для строк вassociation
, которые соответствуют этой родительской строке.для каждого объекта
Child
, затронутого этим немедленным удалением, посколькуpassive_deletes=True
настроен, единице работы не нужно будет пытаться выдавать операторы SELECT для каждой коллекцииChild.parents
, поскольку предполагается, что соответствующие строки вassociation
будут удалены.Затем выдаются утверждения
DELETE
для каждого объектаChild
, который был загружен изParent.children
.
delete-orphan¶
Каскад delete-orphan
добавляет поведение к каскаду delete
, такое, что дочерний объект будет помечен на удаление, когда он отвязан от родителя, а не только когда родитель помечен на удаление. Это обычная особенность при работе со связанным объектом, который «принадлежит» своему родителю с внешним ключом NOT NULL, так что удаление элемента из родительской коллекции приводит к его удалению.
delete-orphan
каскад подразумевает, что каждый дочерний объект может иметь только одного родителя одновременно, и в **подавляющем большинстве случаев настраивается только для отношений один-ко-многим. ** Для гораздо менее распространенного случая настройки на отношения многие-к-одному или многие-ко-многим, сторона «многие» может быть принудительно разрешена только для одного объекта за раз путем настройки аргумента relationship.single_parent
, который устанавливает проверку на стороне Python, гарантирующую, что объект связан только с одним родителем за раз, однако это сильно ограничивает функциональность отношений «многие» и обычно не является желаемым.
См.также
Для отношения <relationship> каскад delete-orphan обычно настраивается только на стороне «один» отношения один-ко-многим, но не на стороне «многие» отношения многие-к-одному или многие-ко-многим. - предыстория распространенного сценария ошибки, связанного с каскадом delete-orphan.
объединить¶
merge
каскад указывает, что операция Session.merge()
должна распространяться от родителя, который является объектом вызова Session.merge()
, вниз к ссылающимся объектам. Этот каскад также включен по умолчанию.
refresh-expire¶
refresh-expire
является необычной опцией, указывающей на то, что операция Session.expire()
должна распространяться от родителя вниз к ссылающимся объектам. При использовании Session.refresh()
, ссылающиеся объекты только истекают, но не обновляются.
исключить¶
Каскад expunge
указывает, что когда родительский объект удаляется из Session
с помощью Session.expunge()
, операция должна быть распространена вниз на ссылающиеся объекты.
Управление каскадом на обратных ссылках¶
Примечание
Этот раздел относится к поведению, которое удалено в SQLAlchemy 2.0. Если установить флаг Session.future
для данного Session
, то будет достигнуто поведение версии 2.0, которое заключается в том, что флаг relationship.cascade_backrefs
игнорируется. Примечания см. в разделе cascade_backrefs behavior deprecated for removal in 2.0.
При использовании 1.x style ORM, каскад save-update по умолчанию происходит на событиях изменения атрибутов, испускаемых из обратных ссылок. Это, вероятно, запутанное утверждение, которое легче описать с помощью демонстрации; оно означает, что, учитывая отображение, подобное этому:
mapper_registry.map_imperatively(
Order, order_table, properties={"items": relationship(Item, backref="order")}
)
Если Order
уже находится в сессии и присвоен атрибуту order
какого-либо Item
, обратная ссылка добавляет Item
к коллекции items
этого Order
, в результате чего происходит каскад save-update
:
>>> o1 = Order()
>>> session.add(o1)
>>> o1 in session
True
>>> i1 = Item()
>>> i1.order = o1
>>> i1 in o1.items
True
>>> i1 in session
True
Это поведение можно отключить с помощью флага relationship.cascade_backrefs
:
mapper_registry.map_imperatively(
Order,
order_table,
properties={"items": relationship(Item, backref="order", cascade_backrefs=False)},
)
Так выше, присвоение i1.order = o1
добавит i1
к коллекции items
коллекции o1
, но не добавит i1
к сессии. Вы, конечно, можете добавить Session.add()
i1
к сессии позднее. Этот вариант может быть полезен в ситуациях, когда объект нужно держать вне сессии до завершения его создания, но при этом ему необходимо дать ассоциации с объектами, уже существующими в целевой сессии.
Когда отношения создаются параметром relationship.backref
на стороне relationship()
, параметр sqlalchemy.orm.cascade_backrefs
может быть установлен на стороне обратной ссылки False
с помощью функции backref()
вместо строки. Например, приведенное выше отношение может быть объявлено:
mapper_registry.map_imperatively(
Order,
order_table,
properties={
"items": relationship(
Item,
backref=backref("order", cascade_backrefs=False),
cascade_backrefs=False,
)
},
)
Это устанавливает поведение cascade_backrefs=False
для обоих отношений.
Примечания по удалению - Удаление объектов, на которые ссылаются коллекции и скалярные отношения¶
ORM в общем случае никогда не изменяет содержимое коллекции или скалярного отношения во время процесса flush. Это означает, что если в вашем классе есть relationship()
, который ссылается на коллекцию объектов или ссылку на один объект, например, many-to-one, содержимое этого атрибута не будет изменено во время процесса flush. Вместо этого, ожидается, что Session
в конечном итоге будет истекшим, либо через поведение expire-on-commit в Session.commit()
, либо через явное использование Session.expire()
. В этот момент любой объект или коллекция, связанная с этим Session
, будет очищена и при следующем обращении загрузится заново.
Распространенная путаница, возникающая при таком поведении, связана с использованием метода Session.delete()
. Когда Session.delete()
вызывается на объект и Session
смывается, строка удаляется из базы данных. Строки, которые ссылаются на целевую строку через внешний ключ, предполагая, что они отслеживаются с помощью relationship()
между двумя сопоставленными типами объектов, также увидят, что их атрибуты внешнего ключа ОБНОВЛЕНЫ на null, или, если настроен каскад удаления, связанные строки также будут удалены. Однако, несмотря на то, что строки, связанные с удаленным объектом, могут быть изменены, никаких изменений в коллекциях, связанных отношениями, или объектных ссылках на объекты, вовлеченные в операцию, в пределах области действия самого удаления не происходит. Это означает, что если объект был членом связанной коллекции, он будет присутствовать на стороне Python до тех пор, пока эта коллекция не закончится. Аналогично, если объект был связан с другим объектом по принципу «многие-к-одному» или «один-к-одному», эта ссылка будет присутствовать на этом объекте до тех пор, пока его срок действия не истечет.
Ниже показано, что после того, как объект Address
помечен для удаления, он все еще присутствует в коллекции, связанной с родительским User
, даже после выполнения команды flush:
>>> address = user.addresses[1]
>>> session.delete(address)
>>> session.flush()
>>> address in user.addresses
True
Когда вышеупомянутая сессия завершена, все атрибуты истекли. При следующем обращении user.addresses
коллекция будет перезагружена, открывая желаемое состояние:
>>> session.commit()
>>> address in user.addresses
False
Существует рецепт для перехвата Session.delete()
и автоматического вызова этого истечения; см. об этом ExpireRelationshipOnFKChange. Однако обычная практика удаления элементов внутри коллекций заключается в том, чтобы отказаться от использования Session.delete()
непосредственно, а вместо этого использовать каскадное поведение для автоматического вызова удаления в результате удаления объекта из родительской коллекции. Каскад delete-orphan
обеспечивает это, как показано в примере ниже:
class User(Base):
__tablename__ = "user"
# ...
addresses = relationship("Address", cascade="all, delete-orphan")
# ...
del user.addresses[1]
session.flush()
Там, где выше, после удаления объекта Address
из коллекции User.addresses
, каскад delete-orphan
имеет эффект пометки объекта Address
для удаления таким же образом, как и передача его в Session.delete()
.
Каскад delete-orphan
также может быть применен к отношениям «многие-к-одному» или «один-к-одному», так что когда объект отсоединяется от своего родителя, он также автоматически помечается для удаления. Использование каскада delete-orphan
на отношениях «многие-к-одному» или «один-к-одному» требует дополнительного флага relationship.single_parent
, который вызывает утверждение, что этот связанный объект не должен одновременно использоваться с любым другим родителем:
class User(Base):
# ...
preference = relationship(
"Preference", cascade="all, delete-orphan", single_parent=True
)
Выше, если гипотетический объект Preference
удален из User
, он будет удален при flush:
some_user.preference = None
session.flush() # will delete the Preference object
См.также
Каскады для получения подробной информации о каскадах.