Каскады

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

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

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

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

  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 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 происходит следующим образом:

  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(), операция должна быть распространена вниз на ссылающиеся объекты.

Управление каскадом на обратных ссылках

Примечание

Этот раздел относится к поведению, которое удалено в 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

См.также

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

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