SQLAlchemy:级联删除

时间:2011-02-17 19:22:54

标签: python database 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个子级,然后提交。接下来,它会删除父项,但子项仍然存在。为什么?如何让孩子级联删除?

10 个答案:

答案 0 :(得分:138)

问题是sqlalchemy认为Child是父母,因为那是你定义你的关系的地方(当然不关心你叫它“Child”)。

如果您在Parent类上定义关系,它将起作用:

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

(注意"Child"作为字符串:当使用声明式样式时允许这样做,这样你就可以引用一个尚未定义的类了。

您可能还想添加delete-orphandelete会导致在删除父项时删除子项,delete-orphan也会删除从父项“已删除”的所有子项,即使父母未被删除)

编辑:刚刚发现:如果确实想要在Child类上定义关系,你可以这样做,但你必须定义级联 on backref (通过显式创建backref),如下所示:

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

(暗示from sqlalchemy.orm import backref

答案 1 :(得分:73)

当您通过session.delete()删除时,@ Steven的asnwer很好,这在我的情况下从未发生过。我注意到大部分时间我通过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 :(得分:65)

很老的帖子,但我只花了一两个小时,所以我想分享我的发现,特别是因为列出的其他一些评论不太正确。

TL; DR

为子表提供外来表或修改现有表,添加onedelete='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的部分。

疑难杂症!

这里有一个重要的警告。请注意我如何使用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 docs中了解有关被动删除的详情。

选项2

你可以做的另一种方法是让SqlAlchemy为你做。这是使用cascade的{​​{1}}参数设置的。如果您在父表上定义了关系,它看起来像这样:

relationship

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

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

同样,这是孩子,所以你必须调用一个名为parent = relationship('Parent', backref=backref('children', cascade='all,delete')) 的方法并将级联数据放在那里。

有了这个,当你删除父行时,SqlAlchemy将实际运行delete语句来清理子行。这可能不如让你使用这个数据库那样有效,所以我不推荐它。

以下是它支持的级联功能的SqlAlchemy docs

答案 3 :(得分:7)

Steven是正确的,你需要显式创建backref,这会导致在父级上应用级联(而不是像在测试场景中那样应用于子级)。

但是,在Child上定义关系并不会使sqlalchemy将Child视为父级。关系定义的位置(子级或父级)无关紧要,链接两个表的外键确定哪个是父级,哪个是子级。

坚持一个约定是有意义的,并且根据史蒂文的回答,我在父母身上定义了我所有的孩子关系。

答案 4 :(得分:5)

我也在努力学习文档,但发现文档字符串本身比手册更容易。例如,如果从sqlalchemy.orm导入关系并执行帮助(关系),它将为您提供可以为级联指定的所有选项。 “delete-orphan”的子弹说:“如果检测到没有父项的子类型的项目,请将其标记为删除。请注意,此选项可防止子类的待处理项在没有父项存在的情况下保留。”

我意识到你的问题更多的是用于定义父子关系的文档。但似乎您可能也遇到了级联选项的问题,因为“所有”包括“删除”。 “删除孤儿”是唯一没有包含在“所有”中的选项。

答案 5 :(得分:4)

史蒂文的答案很可靠。我想指出一个额外的含义。

通过使用relationship,您将使应用层(Flask)负责参照完整性。这意味着不通过Flask访问数据库的其他进程(如数据库实用程序或直接连接到数据库的人)将不会遇到这些约束,并且可能会破坏您工作中难以设计的逻辑数据模型的数据

尽可能使用d512和Alex描述的ForeignKey方法。数据库引擎非常擅长强制实施约束(以不可避免的方式),因此这是维护数据完整性的最佳策略。您需要依赖应用程序来处理数据完整性的唯一时间是数据库无法处理它们,例如不支持外键的SQLite版本。

如果您需要在实体之间创建进一步的链接以启用应用行为,例如导航父子对象关系,请将backrefForeignKey结合使用。

答案 6 :(得分:3)

Alex Okrushko的答案对我来说几乎是最好的。使用ondelete ='CASCADE'和passive_deletes = True组合。但是我必须做些额外的事情才能使其适用于sqlite。

loadAfter()

请确保添加此代码,以确保其适用于sqlite。

'file' => [
    'driver' => 'file',
    'path' => 'Users/path/to',
],

从这里被盗:SQLAlchemy expression language and SQLite's on delete cascade

答案 7 :(得分:0)

Stevan的答案非常完美。但是,如果仍然出现错误。除此之外,其他可能的尝试是-

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

从链接复制-

即使您在模型中指定了级联删除,如果遇到外键依赖性麻烦,也可以快速提示。

使用SQLAlchemy,要指定级联删除,您应在父表上具有cascade ='all,delete'。好的,但是当您执行类似的操作时:

[0]

它实际上会触发有关子表中使用的外键的错误。

我用它来查询对象然后删除它的解决方案:

Item[] inventory = new Item[14];

这应该删除您的父记录以及与之关联的所有子记录。

答案 8 :(得分:0)

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)

使用此设置,级联可以按预期运行。

答案 9 :(得分:0)

尽管这个问题很老,但在谷歌搜索时它首先出现,所以我会发布我的解决方案以添加其他人所说的内容(即使阅读了这里的所有答案,我也花了几个小时) .

正如 d512 所解释的,这都是关于外键的。这让我很惊讶,但并非所有数据库/引擎都支持外键。我正在运行一个 MySQL 数据库。经过长时间的调查,我注意到当我创建新表时,它默认为不支持外键的引擎 (MyISAM)。我所要做的就是在定义表时通过添加 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')