SQLAlchemy:级联删除


116

我必须缺少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个子级,然后提交。接下来,它删除父级,但子级仍然存在。为什么?如何使孩子级联删除?


文档中的这一部分(至少现在是在原始帖子发布3年后)对此很有帮助:docs.sqlalchemy.org/en/rel_0_9/orm/session.html#cascades
Soferio 2014年

Answers:


183

问题是sqlalchemy认为Child是父级的,因为这是您定义关系的地方(当然,它并不关心您将其称为“子级”)。

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

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

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

您可能还想添加delete-orphandelete导致删除父级时删除子级,delete-orphan也删除从父级“删除”的所有子级,即使未删除父级也是如此)

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

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

(暗示from sqlalchemy.orm import backref


6
啊哈,就是这样。我希望文档对此更加明确!
卡尔,

15
嗯 很有帮助。我一直对SQLAlchemy的文档有疑问。
ayaz 2012年


1
@Lyman Zerga:在OP的示例中:如果Child从中删除对象parent.children,应该从数据库中删除该对象,还是仅删除其对父对象的引用(即,将parentid列设置为null,而不是删除行)
史蒂文

1
等等,这relationship并不决定父子设置。使用ForeignKey上表是什么套起来的孩子。relationship父母或孩子都没有关系。
d512

110

当您删除@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)

3
感谢您解释这种差异-我正在尝试使用session.query().filter().delete()并努力查找问题
nighthawk454

4
我必须进行设置passive_deletes='all',以便在删除父级时将其级联删除。使用时passive_deletes=True,在删除父对象之前,子对象已解除关联(父对象设置为NULL),因此数据库级联不执行任何操作。
Milorad Pop-Tosic 2015年

@ MiloradPop-Tosic我已经有3年没有使用SQLAlchemy了,但是阅读文档的方式似乎是Passive_deletes = True仍然是正确的选择。
Alex Okrushko 2015年

2
我可以确认passive_deletes=True在这种情况下可以正常工作。
d512

我在使用Alembic自动生成的修订版(包括在删除时级联)时遇到麻烦-这就是答案。
JNW

105

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

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_tablewhere id= 5 删除一行,那么它将基本上执行

UPDATE child_table SET parent_id = NULL WHERE parent_id = 5

为什么你要这个我不知道。如果许多数据库引擎甚至允许您将有效外键设置为NULL,那么我会感到很惊讶,从而创建了一个孤儿。似乎是个坏主意,但也许有一个用例。无论如何,如果让SqlAlchemy执行此操作,则将防止数据库能够使用ON DELETE CASCADE您设置的清理子级。这是因为它依靠那些外键来知道要删除哪些子行。一旦SqlAlchemy将它们全部设置为NULL,数据库将无法删除它们。设置passive_deletes=Trueprevent可以防止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文档


谢谢您的解释。现在有道理了。
奥丁,

1
为什么Column在子表中声明a ForeignKey('parent.id', ondelete='cascade', onupdate='cascade')也不起作用?我希望孩子在其父表行也被删除时也将被删除。相反,SQLA要么将子级设置为a,要么将parent.id=NULL它们“按原样”保留,但不删除。那是在relationship父级中最初将children = relationship('Parent', backref='parent')或定义为relationship('Parent', backref=backref('parent', passive_deletes=True));之后的。DB cascade在DDL(基于SQLite3的概念证明)中显示规则。有什么想法吗?
code_dredd

1
另外,我应该注意,在使用时backref=backref('parent', passive_deletes=True)会收到以下警告:SAWarning: On Parent.children, 'passive_deletes' is normally configured on one-to-many, one-to-one, many-to-many relationships only. "relationships only." % selfpassive_deletes=True出于某种原因,表明它不喜欢在这种(显而易见的)一对多父子关系中使用。
–'code_dredd

很好的解释。一个问题-是delete多余的cascade='all,delete'吗?
Zaggi

1
@zaggi在中delete是多余的cascade='all,delete',因为根据SQLAlchemy的文档,它all是以下各项的同义词:save-update, merge, refresh-expire, expunge, delete
pmsoltani

7

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

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

不过,遵循一个惯例是有意义的,并且根据史蒂文的回应,我正在定义我所有与父母的孩子关系。


6

我也为文档苦苦挣扎,但是发现文档字符串本身比手册更容易。例如,如果您从sqlalchemy.orm导入关系并执行help(relationship),它将为您提供可以为级联指定的所有选项。项目符号为delete-orphan

如果检测到没有父母的孩子类型的项目,则将其标记为删除。
请注意,此选项可防止在没有父母出席的情况下持久保留孩子班级中待处理的项目。

我知道您的问题更多地在于定义父子关系的文档的方式。但是似乎您也可能对层叠选项有疑问,因为"all"include "delete""delete-orphan"是唯一未包含的选项"all"


help(..)sqlalchemy对象上使用有很大帮助!谢谢 :-))) !PyCharm在上下文停靠区中什么都没有显示,并且显然忘记了检查help。非常感谢!
dmitry_romanov

5

史蒂文的答案很坚定。我想指出另外一个含义。

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

尽可能使用ForeignKeyd512和Alex描述的方法。DB引擎非常擅长真正地执行约束(以不可避免的方式),因此,这是保持数据完整性的最佳策略。您唯一需要依赖应用程序来处理数据完整性的时间是数据库无法处理数据完整性时,例如不支持外键的SQLite版本。

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


2

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)

这将删除您的父记录以及与其关联的所有子记录。


1
.first()需要打电话吗?什么过滤条件返回对象列表,并且所有内容都必须删除?调用不是.first()只获取第一个对象吗?@Prashant
Raju S

2

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的删除级联


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)

通过此设置,级联可以按预期运行。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.