SQLAlchemy: каскадное удаление - PullRequest
       5

SQLAlchemy: каскадное удаление

81 голосов
/ 17 февраля 2011

Мне, должно быть, не хватает чего-то тривиального с опциями каскада SQLAlchemy, потому что я не могу заставить простое каскадное удаление работать правильно - если родительский элемент удален, дочерние элементы сохраняются с null внешними ключами.

Я поместил краткий тестовый пример здесь:

from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key = True)

class Child(Base):
    __tablename__ = "child"
    id = Column(Integer, primary_key = True)
    parentid = Column(Integer, ForeignKey(Parent.id))
    parent = relationship(Parent, cascade = "all,delete", backref = "children")

engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)

session = Session()

parent = Parent()
parent.children.append(Child())
parent.children.append(Child())
parent.children.append(Child())

session.add(parent)
session.commit()

print "Before delete, children = {0}".format(session.query(Child).count())
print "Before delete, parent = {0}".format(session.query(Parent).count())

session.delete(parent)
session.commit()

print "After delete, children = {0}".format(session.query(Child).count())
print "After delete parent = {0}".format(session.query(Parent).count())

session.close()

Вывод:

Before delete, children = 3
Before delete, parent = 1
After delete, children = 3
After delete parent = 0

Существует простое отношение один-ко-многим между Родителем иРебенок.Скрипт создает родителя, добавляет 3 детей, затем фиксирует.Затем он удаляет родителя, но дети сохраняются.Зачем?Как сделать так, чтобы детский каскад удалялся?

Ответы [ 7 ]

143 голосов
/ 17 февраля 2011

Проблема в том, что sqlalchemy рассматривает Child как родителя, потому что именно там вы определили свои отношения (разумеется, вам не важно, что вы назвали их "Дитя").

Если вместо этого вы определите отношение в классе Parent, оно будет работать:

children = relationship("Child", cascade="all,delete", backref="parent")

(примечание "Child" в виде строки: это разрешено при использовании декларативного стиля, так что вы можете ссылаться на класс, который еще не определен)

Вы также можете добавить delete-orphan (delete приводит к удалению потомков при удалении родителя, delete-orphan также удаляет всех потомков, которые были «удалены» из родителя, даже если родитель не Исключен)

РЕДАКТИРОВАТЬ: только что выяснил: если вы действительно хотите определить отношения в классе Child, вы можете сделать это, но вам нужно будет определить каскад на обратном узле (путем явного создания обратного ссылки), например:

parent = relationship(Parent, backref=backref("children", cascade="all,delete"))

(подразумевается from sqlalchemy.orm import backref)

75 голосов
/ 09 октября 2012

@ Ответ Стивена хорош, когда вы удаляете через session.delete(), что никогда не происходит в моем случае. Я заметил, что большую часть времени я удаляю через session.query().filter().delete() (который не помещает элементы в память и удаляет непосредственно из БД). При использовании этого метода sqlalchemy cascade='all, delete' не работает. Хотя есть решение: ON DELETE CASCADE через db (примечание: не все базы данных поддерживают его).

class Child(Base):
    __tablename__ = "children"

    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey("parents.id", ondelete='CASCADE'))

class Parent(Base):
    __tablename__ = "parents"

    id = Column(Integer, primary_key=True)
    child = relationship(Child, backref="parent", passive_deletes=True)
69 голосов
/ 04 августа 2016

Довольно старый пост, но я потратил час или два на это, поэтому я хотел поделиться своим выводом, тем более, что некоторые другие перечисленные комментарии не совсем верны.

TL; DR

Дайте дочерней таблице чужую таблицу или измените существующую, добавив ondelete='CASCADE':

parent_id = db.Column(db.Integer, db.ForeignKey('parent.id', ondelete='CASCADE'))

И один из следующих соотношений:

а) Это в родительской таблице:

children = db.relationship('Child', backref='parent', passive_deletes=True)

b) Или это на дочернем столе:

parent = db.relationship('Parent', backref=backref('children', passive_deletes=True))

Подробнее

Прежде всего, несмотря на то, что говорится в принятом ответе, отношения родитель / ребенок не устанавливаются с помощью relationship, они устанавливаются с помощью ForeignKey. Вы можете поместить relationship в родительский или дочерний стол, и он будет работать нормально. Хотя, как видно из дочерних таблиц, вы должны использовать функцию backref в дополнение к ключевому аргументу.

Вариант 1 (предпочтительно)

Во-вторых, SqlAlchemy поддерживает два разных вида каскадирования. Первый и тот, который я рекомендую, встроен в вашу базу данных и обычно принимает форму ограничения на объявление внешнего ключа. В PostgreSQL это выглядит так:

CONSTRAINT child_parent_id_fkey FOREIGN KEY (parent_id)
REFERENCES parent_table(id) MATCH SIMPLE
ON DELETE CASCADE

Это означает, что при удалении записи из parent_table все соответствующие строки в child_table будут удалены для вас базой данных. Это быстро и надежно и, вероятно, ваш лучший выбор. Вы устанавливаете это в SqlAlchemy через ForeignKey следующим образом (часть определения дочерней таблицы):

parent_id = db.Column(db.Integer, db.ForeignKey('parent.id', ondelete='CASCADE'))
parent = db.relationship('Parent', backref=backref('children', passive_deletes=True))

ondelete='CASCADE' - это деталь, которая создает ON DELETE CASCADE на столе.

Попался!

Здесь есть важное предупреждение. Обратите внимание, как у меня relationship указано с passive_deletes=True? Если у вас этого нет, все это не будет работать. Это потому, что по умолчанию при удалении родительской записи SqlAlchemy делает что-то действительно странное. Он устанавливает внешние ключи всех дочерних строк в NULL. Так что если вы удалите строку из parent_table, где id = 5, то она в основном выполнит

UPDATE child_table SET parent_id = NULL WHERE parent_id = 5

Зачем тебе это, я понятия не имею. Я был бы удивлен, если бы многие движки баз данных даже позволили вам установить действительный внешний ключ в NULL, создавая сироту. Похоже, плохая идея, но, возможно, есть случай использования. В любом случае, если вы позволите SqlAlchemy делать это, вы не сможете очистить базу данных, используя настроенную вами ON DELETE CASCADE. Это потому, что он использует эти внешние ключи, чтобы знать, какие дочерние строки нужно удалить. Как только SqlAlchemy установит их все на NULL, база данных не сможет их удалить. Установка passive_deletes=True предотвращает использование SqlAlchemy NULL внешних ключей.

Подробнее о пассивном удалении можно прочитать в SqlAlchemy документах .

Вариант 2

Другой способ сделать это - позволить SqlAlchemy сделать это за вас. Это устанавливается с помощью аргумента cascade relationship. Если у вас есть отношение, определенное в родительской таблице, оно выглядит так:

children = relationship('Child', cascade='all,delete', backref='parent')

Если отношения с ребенком, вы делаете это так:

parent = relationship('Parent', backref=backref('children', cascade='all,delete'))

Опять же, это ребенок, поэтому вам нужно вызвать метод с именем backref и поместить туда данные каскада.

Имея это в виду, когда вы удаляете родительскую строку, SqlAlchemy фактически запускает операторы удаления, чтобы вы могли очистить дочерние строки. Скорее всего, это будет не так эффективно, как позволить этой базе данных обрабатывать вас, поэтому я не рекомендую это.

Вот SqlAlchemy документы о каскадных функциях, которые он поддерживает.

7 голосов
/ 21 апреля 2013

Стивен прав в том, что вам нужно явно создать обратную ссылку, в результате чего каскад будет применен к родителю (в отличие от того, как он применяется к дочернему элементу, как в тестовом сценарии).

Тем не менее, определение отношений на Child НЕ заставляет sqlalchemy рассматривать Child как родителя. Неважно, где определено отношение (дочерний или родительский), его внешний ключ, который связывает две таблицы, определяет, кто является родительским, а какой - дочерним.

Хотя имеет смысл придерживаться одного соглашения, и, основываясь на ответе Стивена, я определяю все мои дочерние отношения с родителем.

5 голосов
/ 03 июня 2011

Я также боролся с документацией, но обнаружил, что сами строки документации, как правило, проще, чем руководство. Например, если вы импортируете отношения из sqlalchemy.orm и выполняете справку (отношения), это даст вам все параметры, которые вы можете указать для каскада. В надписи «delete-orphan» указано: «если обнаружен элемент дочернего типа без родителя, отметьте его для удаления. Обратите внимание, что этот параметр предотвращает сохранение ожидающего элемента дочернего класса без присутствия родителя».

Я понимаю, что ваша проблема была связана с документацией для определения отношений между родителями и детьми. Но может показаться, что у вас также могут быть проблемы с каскадными параметрами, потому что «все» включает «удаление». «Удалить сироту» - это единственный параметр, который не включен в «все».

4 голосов
/ 28 сентября 2017

Стивен ответ твердый. Я хотел бы указать на дополнительное значение.

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

По возможности, используйте подход ForeignKey, описанный d512 и Alex. Механизм БД очень хорошо справляется с принудительным соблюдением ограничений (неизбежным образом), поэтому это лучшая стратегия для поддержания целостности данных. Единственный раз, когда вам нужно полагаться на приложение для обработки целостности данных, это когда база данных не может их обработать, например версии SQLite, которые не поддерживают внешние ключи.

Если вам необходимо создать дополнительную связь между сущностями, чтобы включить поведение приложений, например, навигацию по отношениям между родителями и дочерними объектами, используйте backref в сочетании с ForeignKey.

0 голосов
/ 21 июня 2019

Ответ Стевана идеален.Но если вы все еще получаете ошибку.Другой возможной попыткой будет: -

http://vincentaudebert.github.io/python/sql/2015/10/09/cascade-delete-sqlalchemy/

Скопировано из ссылки -

Быстрый совет, если у вас возникнут проблемы с зависимостью внешнего ключа, даже есливы указали каскадное удаление в своих моделях.

Используя SQLAlchemy, чтобы указать каскадное удаление, вы должны иметь cascade = 'all, delete' в вашей родительской таблице.Хорошо, но тогда, когда вы выполняете что-то вроде:

session.query(models.yourmodule.YourParentTable).filter(conditions).delete()

Это на самом деле вызывает ошибку о внешнем ключе, используемом в ваших дочерних таблицах.удалите его:

session = models.DBSession()
your_db_object = session.query(models.yourmodule.YourParentTable).filter(conditions).first()
if your_db_object is not None:
    session.delete(your_db_object)

Это должно удалить вашу родительскую запись И все дочерние элементы, связанные с ней.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...