Каскады

Картографы поддерживают концепцию настраиваемого поведения 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.

Использование каскада удаления с отношениями «многие-ко-многим

Опция 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")

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

  1. Приложение вызывает session.delete(my_parent), где my_parent является экземпляром Parent.

  2. Когда Session в следующий раз передает изменения в базу данных, все текущие загруженные элементы в коллекции my_parent.children удаляются ORM, то есть для каждой записи выдается оператор DELETE.

  3. Если коллекция my_parent.children является выгруженной, то никаких утверждений DELETE не выдается. Если бы флаг relationship.passive_deletes был не установлен для этой коллекции relationship(), то было бы выдано утверждение SELECT для выгруженных объектов Child.

  4. Затем выдается оператор DELETE для самой строки my_parent.

  5. Настройка ON DELETE CASCADE на уровне базы данных гарантирует, что все строки в child, которые ссылаются на затронутую строку в parent, также будут удалены.

  6. Экземпляр Parent, на который ссылается my_parent, а также все экземпляры Child, которые были связаны с этим объектом и были загружены (т.е. произошел шаг 2 выше), деассоциируются от Session.

Примечание

Чтобы использовать «ON DELETE CASCADE», базовый движок базы данных должен поддерживать ограничения FOREIGN KEY, и они должны быть принудительными:

Использование внешнего ключа 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 происходит следующим образом:

  1. Объект Parent помечается для удаления с помощью Session.delete().

  2. Когда происходит flush, если коллекция Parent.children не загружена, ORM сначала выпустит оператор SELECT, чтобы загрузить объекты Child, соответствующие Parent.children.

  3. Затем он будет выдавать утверждения DELETE для строк в association, которые соответствуют этой родительской строке.

  4. для каждого объекта Child, затронутого этим немедленным удалением, поскольку passive_deletes=True настроен, единице работы не нужно будет пытаться выдавать операторы SELECT для каждой коллекции Child.parents, поскольку предполагается, что соответствующие строки в association будут удалены.

  5. Затем выдаются утверждения DELETE для каждого объекта Child, который был загружен из Parent.children.

delete-orphan

Каскад delete-orphan добавляет поведение к каскаду delete, такое, что дочерний объект будет помечен на удаление, когда он отвязан от родителя, а не только когда родитель помечен на удаление. Это обычная особенность при работе со связанным объектом, который «принадлежит» своему родителю с внешним ключом NOT NULL, так что удаление элемента из родительской коллекции приводит к его удалению.

delete-orphan каскад подразумевает, что каждый дочерний объект может иметь только одного родителя одновременно, и в **подавляющем большинстве случаев настраивается только для отношений один-ко-многим. ** Для гораздо менее распространенного случая настройки на отношения многие-к-одному или многие-ко-многим, сторона «многие» может быть принудительно разрешена только для одного объекта за раз путем настройки аргумента relationship.single_parent, который устанавливает проверку на стороне Python, гарантирующую, что объект связан только с одним родителем за раз, однако это сильно ограничивает функциональность отношений «многие» и обычно не является желаемым.

объединить

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

См.также

Каскады для получения подробной информации о каскадах.

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