Каскады¶
Картографы поддерживают концепцию настраиваемого поведения 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.scalars(select(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
происходит однонаправленно в контексте двунаправленного отношения, т.е. при использовании параметров relationship.back_populates
или relationship.backref
для создания двух отдельных объектов relationship()
, которые ссылаются друг на друга.
Объект, не связанный с Session
, при назначении атрибута или коллекции на родительский объект, связанный с Session
, будет автоматически добавлен в тот же Session
. Однако та же операция в обратном направлении не будет иметь такого эффекта; объект, не ассоциированный с Session
, на который назначен дочерний объект, ассоциированный с Session
, не приведет к автоматическому добавлению этого родительского объекта к Session
. В целом такое поведение известно как «каскадные обратные ссылки» и представляет собой изменение в поведении, которое было стандартизировано начиная с SQLAlchemy 2.0.
В качестве примера можно привести отображение объектов Order
, которые двунаправленно связаны с серией объектов Item
через отношения Order.items
и Item.order
:
mapper_registry.map_imperatively(
Order,
order_table,
properties={"items": relationship(Item, back_populates="order")},
)
mapper_registry.map_imperatively(
Item,
item_table,
properties={"order": relationship(Order, back_populates="items")},
)
Если Order
уже связан с Session
, а затем создается объект Item
и добавляется в коллекцию Order.items
этого Order
, то Item
будет автоматически каскадирован в тот же Session
:
>>> o1 = Order()
>>> session.add(o1)
>>> o1 in session
True
>>> i1 = Item()
>>> o1.items.append(i1)
>>> o1 is i1.order
True
>>> i1 in session
True
Выше, двунаправленная природа Order.items
и Item.order
означает, что добавление к Order.items
также присваивает к Item.order
. В то же время, каскад save-update
позволял объекту Item
добавляться к тому же Session
, с которым уже был связан родительский Order
.
Однако, если описанная выше операция выполняется в обратном направлении, где Item.order
присваивается, а не добавляется непосредственно к Order.item
, операция каскада в Session
не произойдет автоматически, хотя присвоения объектов Order.items
и Item.order
будут в том же состоянии, что и в предыдущем примере:
>>> o1 = Order()
>>> session.add(o1)
>>> o1 in session
True
>>> i1 = Item()
>>> i1.order = o1
>>> i1 in order.items
True
>>> i1 in session
False
В приведенном выше случае, после создания объекта Item
и установки для него всех необходимых состояний, он должен быть добавлен в Session
явно:
>>> session.add(i1)
В старых версиях SQLAlchemy каскад сохранения-обновления происходил двунаправленно во всех случаях. Затем он был сделан необязательным с помощью опции, известной как cascade_backrefs
. Наконец, в SQLAlchemy 1.4 старое поведение было отменено, а опция cascade_backrefs
была удалена в SQLAlchemy 2.0. Это объясняется тем, что пользователи обычно не считают интуитивно понятным, что присвоение атрибута объекту, показанное выше как присвоение i1.order = o1
, изменит состояние персистентности этого объекта i1
таким образом, что он теперь находится в состоянии ожидания Session
, и часто будут возникать последующие проблемы, когда autoflush будет преждевременно очищать объект и вызывать ошибки в тех случаях, когда данный объект все еще создается и не находится в состоянии готовности к очистке. Также была удалена возможность выбора между однонаправленным и двунаправленным поведением, поскольку эта опция создавала два немного разных способа работы, что увеличивало общую кривую обучения ORM, а также нагрузку на документацию и поддержку пользователей.
См.также
поведение cascade_backrefs deprecated для удаления в 2.0 - информация об изменении поведения для «каскадных обратных ссылок»
удалить¶
Каскад delete
указывает, что когда «родительский» объект помечен на удаление, его связанные «дочерние» объекты также должны быть помечены на удаление. Если, например, у нас есть отношение User.addresses
с настроенным каскадом delete
:
class User(Base):
# ...
addresses = relationship("Address", cascade="all, delete")
Если использовать приведенное выше отображение, у нас есть объект User
и два связанных объекта Address
:
>>> user1 = sess1.scalars(select(User).filter_by(id=1)).first()
>>> address1, address2 = user1.addresses
Если мы пометим user1
для удаления, то после выполнения операции flush address1
и address2
также будут удалены:
>>> sess.delete(user1)
>>> sess.commit()
{execsql}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()
{execsql}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 = mapped_column(Integer, primary_key=True)
children = relationship(
"Child",
secondary=association_table,
back_populates="parents",
cascade="all, delete",
)
class Child(Base):
__tablename__ = "right"
id = mapped_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 = mapped_column(Integer, primary_key=True)
children = relationship(
"Child",
back_populates="parent",
cascade="all, delete",
passive_deletes=True,
)
class Child(Base):
__tablename__ = "child"
id = mapped_column(Integer, primary_key=True)
parent_id = mapped_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
, и они должны быть принудительными:
При использовании MySQL необходимо выбрать соответствующий механизм хранения данных. Подробности см. в разделе Аргументы CREATE TABLE, включая механизмы хранения данных.
При использовании SQLite поддержка внешних ключей должна быть включена явным образом. Подробности см. в разделе Поддержка иностранных ключей.
Использование внешнего ключа 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 = mapped_column(Integer, primary_key=True)
children = relationship(
"Child",
secondary=association_table,
back_populates="parents",
cascade="all, delete",
)
class Child(Base):
__tablename__ = "right"
id = mapped_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()
, операция должна быть распространена вниз на ссылающиеся объекты.
Примечания по удалению - Удаление объектов, на которые ссылаются коллекции и скалярные отношения¶
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
См.также
Каскады для получения подробной информации о каскадах.