SQLAlchemy: 级联删除

对于 SQLAlchemy 的级联选项,我肯定遗漏了一些细节,因为我无法获得一个简单的级联 delete 来正确操作——如果父元素被删除了,那么子元素将使用 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个子代,然后提交。接下来,它删除父级,但子级保留。为什么?我如何使孩子级联删除?

131032 次浏览

问题在于 sqltancy 将 Child视为父级,因为这是您定义关系的地方(当然,它不在乎您将其称为“ Child”)。

如果改为在 Parent类上定义关系,那么它将工作:

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

(注意 "Child"为字符串: 在使用声明式样式时允许这样做,以便您能够引用尚未定义的类)

您可能还需要添加 delete-orphan(当父节点被删除时,delete会导致子节点被删除,delete-orphan还会删除从父节点“删除”的任何子节点,即使父节点没有被删除)

编辑: 刚刚发现: 如果你想定义 Child类上的关系,你可以这样做,但是你必须定义级联 后裁判(通过显式创建 backref) ,像这样:

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

(暗示 from sqlalchemy.orm import backref)

我也很难理解文档,但是发现文档字符串本身比手册更容易理解。例如,如果您从 sqllegy.orm 导入关系并做帮助(关系) ,它将提供所有可以指定用于级联的选项。delete-orphan的子弹上写着:

如果检测到子类型中没有父类型的项,请将其标记为删除。
注意,此选项可防止子类的挂起项成为 在没有父母在场的情况下持续存在。

我知道你的问题在于定义亲子关系的文档方式。但似乎你也可能有一个问题与级联选项,因为 "all"包括 "delete""delete-orphan"是唯一不包含在 "all"中的选项。

@ 当你通过 session.delete()删除时,史蒂文的回答是好的,这在我的情况下从来没有发生过。我注意到,大多数时候我通过 session.query().filter().delete()删除(它不会将元素放入内存,而是直接从 db 中删除)。 使用这种方法 sql 炼金术的 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)

Steven 是正确的,因为你需要明确地创建回溯,这会导致级联被应用到父级上(而不是像测试场景中那样应用到孩子身上)。

然而,在孩子身上定义关系并不意味着孩子就是父母。关系定义在何处(子表或父表)并不重要,重要的是连接两个表的外键,外键确定哪个是父表,哪个是子表。

不过,坚持一种惯例是有道理的,根据史蒂文的回答,我把我所有的孩子关系都定义在父母身上。

相当老的文章,但我只花了一个或两个小时,所以我想分享我的发现,特别是因为其他一些评论列表不太正确。

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中的所有相应行。它快速可靠,可能是你最好的选择。通过 ForeignKey在 SqlAlchemy 中设置如下(子表定义的一部分) :

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的部分。

抓到你了!

这里有一个重要的警告。注意我是如何用 passive_deletes=True指定 relationship的?如果你没有那个,整个事情将不会工作。这是因为默认情况下,当您删除父记录时,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 从 NULL0中输出外键。

您可以在 SqlAlchemy 的文件中阅读有关被动删除的更多信息。

选择二

另一种方法是让 SqlAlchemy 帮你做。这是使用 relationshipcascade参数设置的。如果在父表上定义了关系,那么它看起来像这样:

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

如果关系是在孩子身上,你可以这样做:

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

同样,这是一个子元素,因此您必须调用一个名为 backref的方法,并将级联数据放入其中。

在这种情况下,当您删除父行时,SqlAlchemy 实际上将运行 delete 语句来清理子行。这可能不会像让这个数据库处理那样有效,如果对你来说,所以我不建议这样做。

下面是它所支持的级联特性的 SqlAlchemy 的文件

史蒂文的回答是可靠的。我想指出一个附加的含义。

通过使用 relationship,你可以让应用层(Flask)负责参照完整性。这意味着不通过 Flask 访问数据库的其他进程,比如数据库实用程序或者直接连接到数据库的人,将不会受到这些限制,并且可能会以一种打破你辛苦设计的逻辑数据模型的方式改变你的数据。

只要有可能,就使用 d512和 Alex 描述的 ForeignKey方法。DB 引擎非常善于真正强制约束(以一种不可避免的方式) ,因此这是迄今为止维护数据完整性的最佳策略。只有当数据库无法处理数据完整性时,才需要依赖应用程序来处理数据完整性,例如不支持外键的 SQLite 版本。

如果您需要在实体之间创建进一步的链接以支持应用程序行为,比如导航父子对象关系,那么可以将 backrefForeignKey结合使用。

史蒂文的回答很完美。但是如果你仍然得到错误。另一个可能的尝试是-

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)

这将删除您的父记录和所有与其相关的子记录。

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)

有了这个设置,级联就像预期的那样工作了。

Alex Okrushko 的回答对我来说几乎是最好的。组合使用 ondelete = ‘ CASCADE’和 sive _ delete = 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 删除级联

尽管这个问题已经很老了,但是在谷歌搜索的时候它会第一个出现,所以我会把我的解决方案和其他人说的加起来(我已经花了几个小时在这里读了所有的答案)。

正如 d512所解释的,它完全是关于外键的。这让我很惊讶,但并非所有的数据库/引擎都支持外键。我在运行一个 MySQL 数据库。经过长时间的研究,我注意到当我创建新表时,它默认为一个不支持外键的引擎(MyISAM)。我所要做的就是通过在定义一个 Table 时添加 mysql_engine='InnoDB'来将它设置为 InnoDB。在我的项目中,我使用了一个命令式映射,看起来是这样的:

db.Table('child',
Column('id', Integer, primary_key=True),
# other columns
Column('parent_id',
ForeignKey('parent.id', ondelete="CASCADE")),
mysql_engine='InnoDB')