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