问题: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个子级,然后提交。接下来,它删除父级,但子级仍然存在。为什么?如何使孩子级联删除?
回答 0
问题是sqlalchemy认为Child
是父级的,因为这是您定义关系的地方(当然,它并不关心您将其称为“子级”)。
如果您在Parent
类上定义关系,它将起作用:
children = relationship("Child", cascade="all,delete", backref="parent")
(请注意"Child"
为字符串:使用声明式样式时允许这样做,以便您可以引用尚未定义的类)
您可能还想添加delete-orphan
(delete
导致删除父级时删除子级,delete-orphan
也删除从父级“删除”的所有子级,即使未删除父级也是如此)
编辑:刚刚发现:如果您确实想在Child
类上定义关系,则可以这样做,但是您将必须在backref上定义级联(通过显式创建backref),如下所示:
parent = relationship(Parent, backref=backref("children", cascade="all,delete"))
(暗示from sqlalchemy.orm import backref
)
回答 1
当您删除@Steven的附件时,session.delete()
这是一件好事,对于我而言,这永远不会发生。我注意到大部分时间都是通过删除session.query().filter().delete()
(它不会将元素放入内存中并直接从db中删除)。使用此方法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)
回答 2
很老的帖子,但是我只是花了一两个小时,所以我想分享我的发现,特别是因为列出的其他一些评论不太正确。
TL; DR
给子表一个外部表或修改现有表,并添加ondelete='CASCADE'
:
parent_id = db.Column(db.Integer, db.ForeignKey('parent.id', ondelete='CASCADE'))
和一个下列关系:
a)在父表上:
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
where id
= 5 删除一行,那么它将基本上执行
UPDATE child_table SET parent_id = NULL WHERE parent_id = 5
为什么你要这个我不知道。如果许多数据库引擎甚至允许您将有效外键设置为NULL
,那么我会感到很惊讶,从而创建了一个孤儿。似乎是个坏主意,但也许有一个用例。无论如何,如果让SqlAlchemy执行此操作,则将防止数据库能够使用ON DELETE CASCADE
您设置的清理子级。这是因为它依靠那些外键来知道要删除哪些子行。一旦SqlAlchemy将它们全部设置为NULL
,数据库将无法删除它们。设置passive_deletes=True
prevent可以防止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实际上将运行delete语句供您清理子行。如果您愿意,这可能不如让该数据库处理有效,所以我不建议这样做。
这是有关其支持的级联功能的SqlAlchemy文档。
回答 3
Steven是正确的,因为您需要显式创建backref,这将导致级联被应用到父级(而不是像在测试场景中那样被应用于子级)。
但是,在Child上定义关系不会使sqlalchemy将Child视为父级。定义关系的位置(子级或父级)都无关紧要,它的外键链接两个确定父级和子级的表。
不过,遵循一个惯例是有意义的,并且根据史蒂文的回应,我正在定义我所有与父母的孩子关系。
回答 4
我也为文档苦苦挣扎,但是发现文档字符串本身比手册更容易。例如,如果您从sqlalchemy.orm导入关系并执行help(relationship),它将为您提供可以为级联指定的所有选项。项目符号为delete-orphan
:
如果检测到没有父母的孩子类型的项目,则将其标记为删除。
请注意,此选项可防止在没有父母出席的情况下持久保留孩子Class中待处理的项目。
我知道您的问题更多地在于定义父子关系的文档的方式。但是似乎您也可能对层叠选项有疑问,因为"all"
include "delete"
。 "delete-orphan"
是唯一未包含的选项"all"
。
回答 5
史蒂文的答案很坚定。我想指出另外一个含义。
通过使用relationship
,您将使应用层(Flask)负责引用完整性。这意味着其他不通过Flask访问数据库的进程(例如数据库实用程序或直接连接到数据库的人)将不会遇到这些约束,并且可能以破坏您如此努力设计的逻辑数据模型的方式更改数据。
尽可能使用ForeignKey
d512和Alex描述的方法。DB引擎非常擅长真正地执行约束(以不可避免的方式),因此,这是保持数据完整性的最佳策略。您唯一需要依赖应用程序来处理数据完整性的时间是数据库无法处理数据完整性时,例如不支持外键的SQLite版本。
如果您需要在实体之间创建进一步的链接以启用诸如导航父子对象关系之类的应用行为backref
,请与结合使用ForeignKey
。
回答 6
Stevan的回答是完美的。但是,如果仍然出现错误。在此之上的其他可能的尝试是-
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)
这将删除您的父记录以及与其关联的所有子记录。
回答 7
Alex Okrushko的回答对我来说几乎是最好的。结合使用ondelete =’CASCADE’和passive_deletes = True。但是我必须做些额外的事情才能使其在sqlite中起作用。
Base = declarative_base()
ROOM_TABLE = "roomdata"
FURNITURE_TABLE = "furnituredata"
class DBFurniture(Base):
__tablename__ = FURNITURE_TABLE
id = Column(Integer, primary_key=True)
room_id = Column(Integer, ForeignKey('roomdata.id', ondelete='CASCADE'))
class DBRoom(Base):
__tablename__ = ROOM_TABLE
id = Column(Integer, primary_key=True)
furniture = relationship("DBFurniture", backref="room", passive_deletes=True)
确保添加此代码以确保其适用于sqlite。
from sqlalchemy import event
from sqlalchemy.engine import Engine
from sqlite3 import Connection as SQLite3Connection
@event.listens_for(Engine, "connect")
def _set_sqlite_pragma(dbapi_connection, connection_record):
if isinstance(dbapi_connection, SQLite3Connection):
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON;")
cursor.close()
从这里偷来的:SQLAlchemy表达式语言和SQLite的删除级联
回答 8
TLDR:如果上述解决方案不起作用,请尝试将nullable = False添加到您的列中。
我想在这里为一些可能无法使层叠功能与现有解决方案配合使用的人提供一个小技巧(很棒)。我的工作和示例之间的主要区别是我使用了自动映射。我不确切知道这可能如何影响级联的设置,但是我想指出我使用了它。我也在使用SQLite数据库。
我尝试了这里描述的所有解决方案,但是当删除父行时,子表中的行继续将其外键设置为null。我在这里尝试了所有解决方案都无济于事。但是,一旦我将带有外键的子列设置为nullable = False,级联就可以工作。
在子表上,我添加了:
Column('parent_id', Integer(), ForeignKey('parent.id', ondelete="CASCADE"), nullable=False)
Child.parent = relationship("parent", backref=backref("children", passive_deletes=True)
通过此设置,级联可以按预期运行。