简体   繁体   English

SQLAlchemy:级联删除

[英]SQLAlchemy: cascade delete

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.我一定遗漏了 SQLAlchemy 的级联选项的一些微不足道的东西,因为我无法通过简单的级联删除来正确操作——如果父元素被删除,子元素将保持不变,外键为null

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. Parent 和 Child 之间存在简单的一对多关系。 The script creates a parent, adds 3 children, then commits.该脚本创建一个父级,添加 3 个子级,然后提交。 Next, it deletes the parent, but the children persist.接下来,它删除父级,但子级仍然存在。 Why?为什么? How do I make the children cascade delete?如何让孩子级联删除?

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).问题在于 sqlalchemy 将Child视为父级,因为这是您定义关系的地方(当然,它并不关心您是否称其为“Child”)。

If you define the relationship on the Parent class instead, it will work:如果您在Parent类上定义关系,它将起作用:

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) (注意"Child"作为字符串:使用声明式样式时允许这样做,以便您能够引用尚未定义的类)

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)您可能还想添加delete-orphandelete会导致删除父级时删除子级, delete-orphan也会删除从父级中“删除”的所有子级,即使父级没有被删除)

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

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

(implying from sqlalchemy.orm import backref ) (暗示from sqlalchemy.orm import backref

@Steven's asnwer is good when you are deleting through session.delete() which never happens in my case.当您通过session.delete()删除时,@Steven 的 asnwer 很好,这在我的情况下从未发生过。 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).我注意到大部分时间我通过session.query().filter().delete() (它不会将元素放入内存并直接从数据库中删除)。 Using this method sqlalchemy's cascade='all, delete' doesn't work.使用这种方法sqlalchemy 的cascade='all, delete'不起作用。 There is a solution though: ON DELETE CASCADE through db (note: not all databases support it).不过有一个解决方案:通过 db ON DELETE CASCADE (注意:并非所有数据库都支持它)。

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)

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 TL; 博士

Give the child table a foreign or modify the existing one, adding ondelete='CASCADE' :给子表一个外部或修改现有的,添加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: a) 在父表上:

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

b) Or this on the child table: b)或者在子表上:

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 .首先,不管接受的答案是什么,父/子关系不是通过使用relationship建立的,而是通过使用ForeignKey建立的。 You can put the relationship on either the parent or child tables and it will work fine.您可以将relationship放在父表或子表上,它会正常工作。 Although, apparently on the child tables, you have to use the backref function in addition to the keyword argument.虽然,显然在子表上,除了关键字参数之外,您还必须使用backref函数。

Option 1 (preferred)选项 1(首选)

Second, SqlAlchemy supports two different kinds of cascading.其次,SqlAlchemy 支持两种不同的级联。 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:在 PostgreSQL 中,它看起来像这样:

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.这意味着,当你删除一条记录parent_table ,那么所有相应的行child_table会为你的数据库中删除。 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):您可以通过ForeignKey在 SqlAlchemy 中进行设置,如下所示(子表定义的一部分):

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. ondelete='CASCADE'是在表上创建ON DELETE CASCADE的部分。

Gotcha!知道了!

There's an important caveat here.这里有一个重要的警告。 Notice how I have a relationship specified with passive_deletes=True ?请注意我是如何使用passive_deletes=True指定relationship 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.这是因为默认情况下,当您删除父记录时,SqlAlchemy 会做一些非常奇怪的事情。 It sets the foreign keys of all child rows to NULL .它将所有子行的外键设置为NULL So if you delete a row from parent_table where id = 5, then it will basically execute所以如果你从parent_table中删除id = 5 的一行,那么它基本上会执行

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.如果许多数据库引擎甚至允许您将有效的外键设置为NULL ,从而创建一个孤儿,我会感到惊讶。 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.无论如何,如果您让 SqlAlchemy 这样做,您将阻止数据库使用您设置的ON DELETE CASCADE清理子项。 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.一旦 SqlAlchemy 将它们全部设置为NULL ,数据库就无法删除它们。 Setting the passive_deletes=True prevents SqlAlchemy from NULL ing out the foreign keys.设置passive_deletes=True可以防止SqlAlchemy 将外键设为NULL

You can read more about passive deletes in the SqlAlchemy docs .您可以在SqlAlchemy 文档中阅读有关被动删除的更多信息。

Option 2选项 2

The other way you can do it is to let SqlAlchemy do it for you.另一种方法是让 SqlAlchemy 为你做。 This is set up using the cascade argument of the relationship .这是使用relationshipcascade参数设置的。 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.同样,这是孩子,所以你必须调用一个名为backref的方法并将级联数据放在那里。

With this in place, when you delete a parent row, SqlAlchemy will actually run delete statements for you to clean up the child rows.有了这个,当您删除父行时,SqlAlchemy 实际上会运行删除语句来清理子行。 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.这是关于它支持的级联功能的SqlAlchemy 文档

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

However, defining the relationship on the Child does NOT make sqlalchemy consider Child the parent.但是,在 Child 上定义关系不会使 sqlalchemy 将 Child 视为父级。 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.不过,坚持一个约定是有道理的,根据史蒂文的回应,我将所有孩子的关系都定义在父母身上。

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.例如,如果您从 sqlalchemy.orm 导入关系并执行 help(relationship),它将为您提供可以为级联指定的所有选项。 The bullet for delete-orphan says: delete-orphan的项目符号说:

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" .但您似乎也遇到了级联选项的问题,因为"all"包括"delete" "delete-orphan" is the only option that's not included in "all" . "delete-orphan"是唯一未包含在"all"

Alex Okrushko answer almost worked best for me. Alex Okrushko 的回答几乎对我来说效果最好。 Used ondelete='CASCADE' and passive_deletes=True combined.使用 ondelete='CASCADE' 和passive_deletes=True 组合。 But I had to do something extra to make it work for sqlite.但是我必须做一些额外的事情才能使其适用于 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.确保添加此代码以确保它适用于 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从这里被盗: SQLAlchemy 表达式语言和 SQLite 的删除级联

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

Whenever possible, use the ForeignKey approach described by d512 and Alex.尽可能使用 d512 和 Alex 描述的ForeignKey方法。 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. DB 引擎非常擅长真正强制执行约束(以不可避免的方式),因此这是迄今为止维护数据完整性的最佳策略。 The only time you need to rely on an app to handle data integrity is when the database can't handle them, eg versions of SQLite that don't support foreign keys.唯一需要依赖应用程序来处理数据完整性的时候是数据库无法处理它们的时候,例如不支持外键的 SQLite 版本。

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 .如果您需要在实体之间创建进一步的链接以启用应用行为,例如导航父子对象关系, backrefForeignKey结合使用。

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/ 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.使用 SQLAlchemy,要指定级联删除,您应该在父表上设置cascade='all, delete' 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.这应该删除您的父记录以及与之关联的所有子记录。

TLDR: If the above solutions don't work, try adding nullable=False to your column. TLDR:如果上述解决方案不起作用,请尝试将 nullable=False 添加到您的列中。

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.我的工作和示例之间的主要区别在于我使用了 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.我也在使用 SQLite 数据库。

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.我尝试了此处描述的所有解决方案,但是当删除父行时,我的子表中的行继续将其外键设置为 null。 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.但是,一旦我将带有外键的子列设置为 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.通过这种设置,级联按预期运行。

Even tho this question is very old, it comes up first when searched for in Google so I'll post my solution to add up to what others said (I've spent few hours even after reading all the answers in here).即使这个问题很老,它在谷歌中搜索时首先出现,所以我会发布我的解决方案以补充其他人所说的内容(即使阅读了这里的所有答案,我也花了几个小时)。

As d512 explained, it is all about Foreign Keys.正如 d512 所解释的,这都是关于外键的。 It was quite a surprise to me but not all databases / engines support Foreign Keys.这让我很惊讶,但并非所有数据库/引擎都支持外键。 I'm running a MySQL database.我正在运行一个 MySQL 数据库。 After long investigation, I noticed that when I create new table it defaults to an engine (MyISAM) that doesn't support Foreign Keys.经过长时间的调查,我注意到当我创建新表时,它默认为不支持外键的引擎 (MyISAM)。 All I had to do was to set it to InnoDB by adding mysql_engine='InnoDB' when defining a Table.我所要做的就是在定义表时通过添加mysql_engine='InnoDB'将其设置为 InnoDB。 In my project I'm using an imperative mapping and it looks like so:在我的项目中,我使用了一个命令式映射,它看起来像这样:

db.Table('child',
    Column('id', Integer, primary_key=True),
    # other columns
    Column('parent_id',
           ForeignKey('parent.id', ondelete="CASCADE")),
    mysql_engine='InnoDB')

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM