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

I must be missing something trivial with SQLAlchemy’s cascade options because I cannot get a simple cascade delete to operate correctly — if a parent element is a deleted, the children persist, with null foreign keys.

I’ve put a concise test case here:

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()

Output:

Before delete, children = 3
Before delete, parent = 1
After delete, children = 3
After delete parent = 0

There is a simple, one-to-many relationship between Parent and Child. The script creates a parent, adds 3 children, then commits. Next, it deletes the parent, but the children persist. Why? How do I make the children cascade delete?


回答 0

问题是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

The problem is that sqlalchemy considers Child as the parent, because that is where you defined your relationship (it doesn’t care that you called it “Child” of course).

If you define the relationship on the Parent class instead, it will work:

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

(note "Child" as a string: this is allowed when using the declarative style, so that you are able to refer to a class that is not yet defined)

You might want to add delete-orphan as well (delete causes children to be deleted when the parent gets deleted, delete-orphan also deletes any children that were “removed” from the parent, even if the parent is not deleted)

EDIT: just found out: if you really want to define the relationship on the Child class, you can do so, but you will have to define the cascade on the backref (by creating the backref explicitly), like this:

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

(implying from sqlalchemy.orm import backref)


回答 1

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

@Steven’s asnwer is good when you are deleting through session.delete() which never happens in my case. I noticed that most of the time I delete through session.query().filter().delete() (which doesn’t put elements in the memory and deletes directly from db). Using this method sqlalchemy’s cascade='all, delete' doesn’t work. There is a solution though: ON DELETE CASCADE through db (note: not all databases support it).

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

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

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文档

Pretty old post, but I just spent an hour or two on this, so I wanted to share my finding, especially since some of the other comments listed aren’t quite right.

TL;DR

Give the child table a foreign or modify the existing one, adding ondelete='CASCADE':

parent_id = db.Column(db.Integer, db.ForeignKey('parent.id', ondelete='CASCADE'))

And one of the following relationships:

a) This on the parent table:

children = db.relationship('Child', backref='parent', passive_deletes=True)

b) Or this on the child table:

parent = db.relationship('Parent', backref=backref('children', passive_deletes=True))

Details

First off, despite what the accepted answer says, the parent/child relationship is not established by using relationship, it’s established by using ForeignKey. You can put the relationship on either the parent or child tables and it will work fine. Although, apparently on the child tables, you have to use the backref function in addition to the keyword argument.

Option 1 (preferred)

Second, SqlAlchemy supports two different kinds of cascading. The first, and the one I recommend, is built into your database and usually takes the form of a constraint on the foreign key declaration. In PostgreSQL it looks like this:

CONSTRAINT child_parent_id_fkey FOREIGN KEY (parent_id)
REFERENCES parent_table(id) MATCH SIMPLE
ON DELETE CASCADE

This means that when you delete a record from parent_table, then all the corresponding rows in child_table will be deleted for you by the database. It’s fast and reliable and probably your best bet. You set this up in SqlAlchemy through ForeignKey like this (part of the child table definition):

parent_id = db.Column(db.Integer, db.ForeignKey('parent.id', ondelete='CASCADE'))
parent = db.relationship('Parent', backref=backref('children', passive_deletes=True))

The ondelete='CASCADE' is the part that creates the ON DELETE CASCADE on the table.

Gotcha!

There’s an important caveat here. Notice how I have a relationship specified with passive_deletes=True? If you don’t have that, the entire thing will not work. This is because by default when you delete a parent record SqlAlchemy does something really weird. It sets the foreign keys of all child rows to NULL. So if you delete a row from parent_table where id = 5, then it will basically execute

UPDATE child_table SET parent_id = NULL WHERE parent_id = 5

Why you would want this I have no idea. I’d be surprised if many database engines even allowed you to set a valid foreign key to NULL, creating an orphan. Seems like a bad idea, but maybe there’s a use case. Anyway, if you let SqlAlchemy do this, you will prevent the database from being able to clean up the children using the ON DELETE CASCADE that you set up. This is because it relies on those foreign keys to know which child rows to delete. Once SqlAlchemy has set them all to NULL, the database can’t delete them. Setting the passive_deletes=True prevents SqlAlchemy from NULLing out the foreign keys.

You can read more about passive deletes in the SqlAlchemy docs.

Option 2

The other way you can do it is to let SqlAlchemy do it for you. This is set up using the cascade argument of the relationship. If you have the relationship defined on the parent table, it looks like this:

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

If the relationship is on the child, you do it like this:

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

Again, this is the child so you have to call a method called backref and putting the cascade data in there.

With this in place, when you delete a parent row, SqlAlchemy will actually run delete statements for you to clean up the child rows. This will likely not be as efficient as letting this database handle if for you so I don’t recommend it.

Here are the SqlAlchemy docs on the cascading features it supports.


回答 3

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

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

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

Steven is correct in that you need to explicitly create the backref, this results in the cascade being applied on the parent (as opposed to it being applied to the child like in the test scenario).

However, defining the relationship on the Child does NOT make sqlalchemy consider Child the parent. It doesn’t matter where the relationship is defined (child or parent), its the foreign key that links the two tables that determines which is the parent and which is the child.

It makes sense to stick to one convention though, and based on Steven’s response, I’m defining all my child relationships on the parent.


回答 4

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

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

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

I struggled with the documentation as well, but found that the docstrings themselves tend to be easier than the manual. For example, if you import relationship from sqlalchemy.orm and do help(relationship), it will give you all the options you can specify for cascade. The bullet for delete-orphan says:

if an item of the child’s type with no parent is detected, mark it for deletion.
Note that this option prevents a pending item of the child’s class from being persisted without a parent present.

I realize your issue was more with the way the documentation for defining parent-child relationships. But it seemed that you might also be having a problem with the cascade options, because "all" includes "delete". "delete-orphan" is the only option that’s not included in "all".


回答 5

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

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

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

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

Steven’s answer is solid. I’d like to point out an additional implication.

By using relationship, you’re making the app layer (Flask) responsible for referential integrity. That means other processes that access the database not through Flask, like a database utility or a person connecting to the database directly, will not experience those constraints and could change your data in a way that breaks the logical data model you worked so hard to design.

Whenever possible, use the ForeignKey approach described by d512 and Alex. The DB engine is very good at truly enforcing constraints (in an unavoidable way), so this is by far the best strategy for maintaining data integrity. The only time you need to rely on an app to handle data integrity is when the database can’t handle them, e.g. versions of SQLite that don’t support foreign keys.

If you need to create further linkage among entities to enable app behaviors like navigating parent-child object relationships, use backref in conjunction with ForeignKey.


回答 6

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)

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

Answer by Stevan is perfect. But if you are still getting the error. Other possible try on top of that would be –

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

Copied from the link-

Quick tip if you get in trouble with a foreign key dependency even if you have specified a cascade delete in your models.

Using SQLAlchemy, to specify a cascade delete you should have cascade='all, delete' on your parent table. Ok but then when you execute something like:

session.query(models.yourmodule.YourParentTable).filter(conditions).delete()

It actually triggers an error about a foreign key used in your children tables.

The solution I used it to query the object and then delete it:

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)

This should delete your parent record AND all the children associated with it.


回答 7

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

Alex Okrushko answer almost worked best for me. Used ondelete=’CASCADE’ and passive_deletes=True combined. But I had to do something extra to make it work for 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)

Make sure to add this code to ensure it works for 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()

Stolen from here: SQLAlchemy expression language and SQLite’s on delete cascade


回答 8

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)

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

TLDR: If the above solutions don’t work, try adding nullable=False to your column.

I’d like to add a small point here for some people who may not get the cascade function to work with the existing solutions (which are great). The main difference between my work and the example was that I used automap. I do not know exactly how that might interfere with the setup of cascades, but I want to note that I used it. I am also working with a SQLite database.

I tried every solution described here, but rows in my child table continued to have their foreign key set to null when the parent row was deleted. I’d tried all the solutions here to no avail. However, the cascade worked once I set the child column with the foreign key to nullable = False.

On the child table, I added:

Column('parent_id', Integer(), ForeignKey('parent.id', ondelete="CASCADE"), nullable=False)
Child.parent = relationship("parent", backref=backref("children", passive_deletes=True)

With this setup, the cascade functioned as expected.


声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。