SQLAlchemy:级联删除

car*_*arl 88 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()
Run Code Online (Sandbox Code Playgroud)

输出:

Before delete, children = 3
Before delete, parent = 1
After delete, children = 3
After delete parent = 0
Run Code Online (Sandbox Code Playgroud)

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

Ste*_*ven 151

问题是sqlalchemy认为Child是父母,因为那是你定义你的关系的地方(当然不关心你称之为"孩子").

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

children = relationship("Child", cascade="all,delete", backref="parent")
Run Code Online (Sandbox Code Playgroud)

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

您可能还想添加delete-orphan(delete导致删除父项时删除子项,delete-orphan还删除从父项"删除"的所有子项,即使父项未删除)

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

parent = relationship(Parent, backref=backref("children", cascade="all,delete"))
Run Code Online (Sandbox Code Playgroud)

(暗示from sqlalchemy.orm import backref)

  • 埃.很有帮助.我一直有SQLAlchemy文档的问题. (9认同)
  • 啊哈,就是这样.我希望文档对此更加明确! (4认同)
  • 当前文档中对此进行了很好的解释http://docs.sqlalchemy.org/en/rel_0_9/orm/cascades.html (4认同)
  • 等等,“关系”并不决定父子设置。在表上使用 `ForeignKey` 会将其设置为子表。“关系”是针对父母还是子女并不重要。 (2认同)

Ale*_*hko 84

当你删除时,@ Steven的asnwer很好,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)
Run Code Online (Sandbox Code Playgroud)

  • 感谢您解释这种差异 - 我试图使用 `session.query().filter().delete()` 并努力寻找问题 (4认同)
  • 我必须设置`passive_deletes ='all'`,以便在删除父级时让数据库级联删除子级.使用`passive_deletes = True`,在删除父对象之前,子对象被取消关联(父对象设置为NULL),因此数据库级联没有做任何事情. (3认同)
  • 我可以确认`passive_deletes = True`在这种情况下可以正常工作. (2认同)
  • 因为我在完成删除时已经三次得到这个答案:对于 SQLite,启用foreign_keys 至关重要。简而言之:“cursor.execute('PRAGMAforeign_keys=ON')”——使用“event.listens_for”,如 https://docs.sqlalchemy.org/en/13/dialects/sqlite.html#sqlite-foreign 中所述-keys(编辑:并在另一个答案中解释/sf/answers/4362909561/) (2认同)
  • 希望我能不止一次地赞成这个答案——我花了太长时间才弄清楚`x = session.query(T).all(); [session.delete(y) for y in x]` 与 `session.query(T).delete()` 不同。当使用flask_sqlalchemy时,这一点尤其不明显,它为您提供了`T.query.delete()`,它看起来确实_应该_考虑到出于某种原因的关系。 (2认同)

d51*_*512 78

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

TL; DR

给子表一个外来的或修改现有的表,添加ondelete='CASCADE':

parent_id = db.Column(db.Integer, db.ForeignKey('parent.id', ondelete='CASCADE'))
Run Code Online (Sandbox Code Playgroud)

一个下列关系:

a)在父表上:

children = db.relationship('Child', backref='parent', passive_deletes=True)
Run Code Online (Sandbox Code Playgroud)

b)或者在子表上:

parent = db.relationship('Parent', backref=backref('children', passive_deletes=True))
Run Code Online (Sandbox Code Playgroud)

细节

首先,尽管接受的答案说明,父/子关系不是通过使用建立的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
Run Code Online (Sandbox Code Playgroud)

这意味着当您从中删除记录时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))
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

为什么你会想要这个我不知道.如果许多数据库引擎甚至允许你设置一个有效的外键NULL,创建一个孤儿,我会感到惊讶.看起来似乎是一个坏主意,但也许有一个用例.无论如何,如果你让SqlAlchemy这样做,你将阻止数据库使用ON DELETE CASCADE你设置的子程序清理子进程.这是因为它依赖这些外键来知道要删除哪些子行.一旦SqlAlchemy将它们全部设置为NULL,数据库就无法删除它们.设置passive_deletes=True阻止SqlAlchemy NULL输出外键.

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

选项2

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

children = relationship('Child', cascade='all,delete', backref='parent')
Run Code Online (Sandbox Code Playgroud)

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

parent = relationship('Parent', backref=backref('children', cascade='all,delete'))
Run Code Online (Sandbox Code Playgroud)

同样,这是孩子所以你必须调用一个被调用的方法backref并将级联数据放在那里.

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

以下是它支持的级联功能的SqlAlchemy文档.

  • 为什么在子表中将“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 的概念验证)。想法? (2认同)
  • @zaggi `delete` 在 `cascade='all,delete'` 中是多余的,因为根据 [SQLAlchemy 的文档](https://docs.sqlalchemy.org/en/13/orm/cascades.html#cascades), `all` 是`save-update、merge、refresh-expire、expunge、delete`的同义词: (2认同)

小智 7

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

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

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


stu*_*ent 6

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)
Run Code Online (Sandbox Code Playgroud)

确保添加此代码以确保它适用于 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()
Run Code Online (Sandbox Code Playgroud)

从这里被盗:SQLAlchemy 表达式语言和 SQLite 的删除级联


Pro*_*ane 5

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

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


Chr*_*son 5

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

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

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

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