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



Я должен пропустить что-то тривиальное с параметрами каскада 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 дочерних элемента, а затем фиксирует. Затем он удаляет родитель, но дети упорствуют. Зачем? Как сделать так, чтобы дети каскадно удалялись?

1422   6  

6 ответов:

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

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

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

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

вы можете добавить delete-orphan а (delete вызывает удаление детей при удалении родителя,delete-orphan также удаляет все дочерние элементы, которые были "удалены" из родителя, даже если родитель не удален)

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

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

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

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

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)

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

TL; DR

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

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

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

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

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

б) или это на ребенка таблица:

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))

The 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 от NULLing из внешних ключей.

вы можете прочитать больше о пассивных удалениях в SQLAlchemy docs.

2

другой способ сделать это-позволить SqlAlchemy сделать это за вас. Это настраивается с помощью

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

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

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

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

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

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

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

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

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

Comments

    Ничего не найдено.