SQLAlchemy:级联删除

Posted

技术标签:

【中文标题】SQLAlchemy:级联删除【英文标题】:SQLAlchemy: cascade delete 【发布时间】:2011-06-29 08:18:18 【问题描述】:

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

Parent 和 Child 之间存在简单的一对多关系。该脚本创建一个父级,添加 3 个子级,然后提交。接下来,它会删除父级,但子级仍然存在。为什么?如何让子级级联删除?

【问题讨论】:

文档中的这一部分(至少现在,在原始帖子之后的 3 年后)似乎对此很有帮助:docs.sqlalchemy.org/en/rel_0_9/orm/session.html#cascades 我认为session.delete(parent)之后的session.commit()是不必要的。 【参考方案1】:

问题在于 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

【讨论】:

啊哈,就是这样。我希望文档对此更加明确! 是的。非常有帮助。我一直对 SQLAlchemy 的文档有疑问。 这在当前文档docs.sqlalchemy.org/en/rel_0_9/orm/cascades.html中有很好的解释 @Lyman Zerga:在 OP 的示例中:如果您从 parent.children 中删除 Child 对象,应该从数据库中删除该对象,还是只删除它对父对象的引用(即,将parentid 列设置为空,而不是删除行) 等等,relationship 并不规定父子设置。在桌子上使用ForeignKey 就是将其设置为孩子。 relationship 是在父母身上还是在孩子身上都没有关系。【参考方案2】:

@Steven 的 asnwer 在您通过 session.delete() 删除时很好,这在我的情况下从未发生过。我注意到大多数时候我通过session.query().filter().delete() 删除(它不会将元素放入内存并直接从数据库中删除)。 使用这种方法 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)

【讨论】:

感谢您解释这种差异 - 我尝试使用 session.query().filter().delete() 并努力寻找问题 我必须设置passive_deletes='all',以便在删除父级时让子级被数据库级联删除。使用passive_deletes=True,子对象在父对象被删除之前被解除关联(父对象设置为NULL),因此数据库级联没有做任何事情。 我可以确认passive_deletes=True 在这种情况下可以正常工作。 因为在完成删除操作时,我现在已经回答了 3 次:对于 SQLite,启用 foreign_keys 至关重要。简而言之:cursor.execute('PRAGMA foreign_keys=ON') -- 使用event.listens_for,如docs.sqlalchemy.org/en/13/dialects/… 中所述(编辑:并在另一个答案***.com/a/62327279 中解释) 希望我能多次支持这个答案 - 我花了很长时间才发现 x = session.query(T).all(); [session.delete(y) for y in x]session.query(T).delete() 不同。使用flask_sqlalchemy 时尤其不明显,它会为您提供T.query.delete(),它看起来确实应该出于某种原因考虑到这些关系。【参考方案3】:

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

TL;DR

给子表一个foreign或者修改现有的,添加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 NULL 输出外键。

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

选项 2

您可以这样做的另一种方法是让 SqlAlchemy 为您完成。这是使用relationshipcascade 参数设置的。如果您在父表上定义了关系,则如下所示:

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

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

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

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

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

这里是SqlAlchemy docs,它支持的级联功能。

【讨论】:

感谢您的解释。现在说得通了。 为什么在子表中将Column 声明为ForeignKey('parent.id', ondelete='cascade', onupdate='cascade') 也不起作用?我希望当他们的父表行也被删除时,孩子也会被删除。相反,SQLA 要么将子级设置为parent.id=NULL,要么将它们保持“原样”,但不删除。那是在最初将父级中的relationship 定义为children = relationship('Parent', backref='parent')relationship('Parent', backref=backref('parent', passive_deletes=True)) 之后; DB 在 DDL(基于 SQLite3 的概念证明)中显示 cascade 规则。想法? 另外,我应该注意,当我使用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." % self,这表明它不喜欢在这个(明显的)一对一中使用passive_deletes=True - 由于某种原因,许多父子关系。 很好的解释。一个问题 - deletecascade='all,delete' 中是多余的吗? @zaggi deletecascade='all,delete' 中是多余的,因为根据SQLAlchemy's docs,all 是:save-update, merge, refresh-expire, expunge, delete 的同义词【参考方案4】:

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

但是,在 Child 上定义关系不会使 sqlalchemy 将 Child 视为父级。在哪里定义关系(子或父)并不重要,它是链接两个表的外键,它决定了哪个是父表,哪个是子表。

不过,坚持一个约定是有道理的,根据 Steven 的回答,我将我所有的孩子关系都定义在父母身上。

【讨论】:

【参考方案5】:

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 expression language and SQLite's on delete cascade

【讨论】:

【参考方案6】:

史蒂文的回答很中肯。我想指出一个额外的含义。

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

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

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

【讨论】:

【参考方案7】:

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

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

我意识到您的问题更多在于定义父子关系的文档的方式。但您似乎也对级联选项有疑问,因为"all" 包含"delete""delete-orphan" 是唯一未包含在 "all" 中的选项。

【讨论】:

sqlalchemy 对象上使用help(..) 有很大帮助!谢谢 :-))) ! PyCharm 在上下文停靠栏中没有显示任何内容,并且显然忘记检查help。非常感谢!【参考方案8】:

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)

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

【讨论】:

是否需要调用.first()?什么过滤条件返回对象列表,并且必须删除所有内容?调用.first() 不是只得到第一个对象吗? @Prashant 我遇到过这个问题。我想在 pytest 夹具中清除我的整个表,但 session.query(Parent).delete() 似乎没有使用外键。查询对象然后分别遍历每个对象似乎不是正确的解决方案,至少它不应该是....【参考方案9】:

TLDR:如果上述解决方案不起作用,请尝试在您的列中添加 nullable=False。

我想在这里为一些可能无法使级联功能与现有解决方案一起工作的人补充一点(这很棒)。我的工作与示例之间的主要区别在于我使用了 automap。我不知道这会如何干扰级联的设置,但我想指出我使用了它。我也在使用 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)

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

【讨论】:

【参考方案10】:

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

正如 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')

【讨论】:

以上是关于SQLAlchemy:级联删除的主要内容,如果未能解决你的问题,请参考以下文章

Flask 学习-84.Flask-SQLAlchemy 一对多关系级联删除

Flask 学习-84.Flask-SQLAlchemy 一对多关系级联删除

sql级联更新和级联删除不起作用

如何理解access设置中的“级联更新”和“级联删除”?

SQL 级联删除与级联更新的方法

sql 级联删除问题