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

親と子の間には、単純な1対多の関係があります。スクリプトは親を作成し、3つの子を追加して、コミットします。次に、親を削除しますが、子は存続します。どうして?子をカスケード削除するにはどうすればよいですか?


このドキュメント内のセクション(少なくとも今、3年後にオリジナルのポストの後)この上で非常に役に立つようだ:docs.sqlalchemy.org/en/rel_0_9/orm/session.html#cascades
Soferio

回答:


183

問題は、sqlalchemy Childが親と見なすことです。これは、関係を定義した場所だからです(もちろん、「子」と呼んでも構いません)。

Parent代わりにクラスで関係を定義すると、機能します。

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

"Child"文字列として注意:これは宣言スタイルを使用する場合に許可されるため、まだ定義されていないクラスを参照できます)

追加するdelete-orphanこともできます(delete親が削除されると子が削除され、親が削除されてdelete-orphanいなくても、親から「削除」された子も削除されます)

編集:ちょうど見つけた:クラスの関係を本当に定義したい場合Childはそれを行うことができますが、次のように(明示的にバックリファレンスを作成することにより)バックリファレンスにカスケードを定義する必要があります:

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

(暗示from sqlalchemy.orm import backref


6
あは、これです。ドキュメンテーションがこれについてもっと明確になってほしいです!
カール

15
はい。非常に役立ちます。SQLAlchemyのドキュメントには常に問題がありました。
ayaz

1
これはよく、現在のドキュメントの中で説明しているdocs.sqlalchemy.org/en/rel_0_9/orm/cascades.html
エポック

1
@Lyman Zerga:OPの例:Childオブジェクトをから削除する場合parent.children、そのオブジェクトをデータベースから削除するか、またはそのオブジェクトへの参照のみを削除する必要があります(つまりparentid、行を削除する代わりに列をnullに設定します)
スティーブン

1
待って、それrelationshipは親子の設定を指示しません。ForeignKeyテーブルで使用すると、子として設定されます。relationshipが親と子のどちらにあるかは関係ありません。
d512

110

@Stevenのasnwerは、削除する場合に適していますsession.delete()。ほとんどの場合、削除することに気づきsession.query().filter().delete()ました(メモリに要素を配置せず、dbから直接削除します)。この方法を使用すると、sqlalchemy cascade='all, delete'は機能しません。ただし、解決策があります:ON DELETE CASCADEdbを介して(注:すべてのデータベースがそれをサポートしているわけではありません)。

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

@ MiloradPop-Tosic SQLAlchemyを3年以上使用していませんが、ドキュメントを読むと passive_deletes = Trueのように見えます。
Alex Okrushko、2015

2
passive_deletes=Trueこのシナリオで正しく動作することを確認できます。
d512

削除時のカスケードが含まれているalembic自動生成リビジョンで問題が発生しました-これが答えでした。
JNW

104

かなり古い投稿ですが、私はこれに1、2時間費やしただけなので、特に他のコメントの一部が正しくないため、結果を共有したいと思いました。

TL; DR

子テーブルに外部を与えるか、既存のものを変更して、以下を追加しondelete='CASCADE'ます。

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

そして、次の関係の1つ

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は2種類のカスケードをサポートしています。最初のもの、そして私がお勧めするものはデータベースに組み込まれており、通常は外部キー宣言に対する制約の形をとります。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=True、SqlAlchemy NULLが外部キーを出力できなくなります。

パッシブ削除の詳細については、SqlAlchemyのドキュメントをご覧ください

オプション2

もう1つの方法は、SqlAlchemyに任せることです。これはのcascade引数を使用して設定されrelationshipます。親テーブルにリレーションシップが定義されている場合、次のようになります。

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

関係が子供の場合、次のようにします。

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

繰り返しますが、これは子なので、呼び出されたメソッドを呼び出しbackrefてカスケードデータをそこに配置する必要があります。

これにより、親行を削除すると、SqlAlchemyが実際に削除ステートメントを実行して、子行をクリーンアップします。これは、このデータベースで処理する場合ほど効率的ではないため、お勧めしません。

サポートされているカスケード機能に関する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はcascadeDDL(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何らかの理由で、この(明白な)1対多の親子関係でのの使用が望ましくないことを示唆しています。
code_dredd

素晴らしい説明。1つの質問-はdelete冗長cascade='all,delete'ですか?
zaggi

1
@zaggiはdelete、冗長IS cascade='all,delete'に応じているので、SQLAlchemyののドキュメントallの同義語である:save-update, merge, refresh-expire, expunge, delete
pmsoltani

7

Stevenは、後方参照を明示的に作成する必要があるという点で正しいです。これにより、カスケードが親に適用されます(テストシナリオのように子に適用されるのではなく)。

ただし、子の関係を定義しても、sqlalchemyは子を親と見なしません。リレーションシップが定義されている場所(子または親)は関係ありません。どちらが親であり、どちらが子であるかを決定する2つのテーブルをリンクする外部キーです。

ただし、1つの規則に固執することは理にかなっており、Stevenの応答に基づいて、私はすべての子の関係を親に定義しています。


6

私もドキュメンテーションに苦労しましたが、docstrings自体はマニュアルよりも簡単であることがわかりました。たとえば、sqlalchemy.ormから関係をインポートし、help(relationship)を実行すると、カスケードに指定できるすべてのオプションが提供されます。の弾丸はdelete-orphan言う:

親のない子のタイプのアイテムが検出された場合は、削除のマークを付けます。
このオプションは、子のクラスの保留中のアイテムが親の存在なしで永続化されないようにすることに注意してください。

あなたの問題は、親子関係を定義するためのドキュメントの方法にもっとあったと思います。が"all"含まれて"delete"いるため、カスケードオプションにも問題があるようです。 "delete-orphan"に含まれていない唯一のオプションです"all"


使用help(..)sqlalchemyのオブジェクトは、多くのことができます!ありがとう:-)))!PyCharmはコンテキストドックに何も表示せず、明らかにを確認するのを忘れていましたhelp。本当にありがとうございました!
dmitry_romanov

5

スティーブンの答えはしっかりしています。追加の影響を指摘したいと思います。

を使用relationshipすることで、アプリレイヤー(Flask)が参照整合性を担当するようになります。つまり、データベースユーティリティやデータベースに直接接続している人など、Flaskを介さずにデータベースにアクセスする他のプロセスでは、これらの制約が発生せず、設計が非常に困難だった論理データモデルを壊すような方法でデータが変更される可能性があります。

可能な限り、ForeignKeyd512とAlexによって説明されているアプローチを使用してください。DBエンジンは本当に(やむを得ない方法で)制約を強制するのに非常に優れているため、これはデータの整合性を維持するための最善の戦略です。データの整合性を処理するためにアプリに依存する必要があるのは、データベースがそれらを処理できないときだけです。たとえば、外部キーをサポートしないSQLiteのバージョンです。

エンティティ間のリンクをさらに作成backrefして、親子オブジェクトの関係のナビゲートなどのアプリの動作を有効にする必要がある場合は、と組み合わせて使用しForeignKeyます。


2

ステヴァンの答えは完璧です。しかし、それでもエラーが発生する場合。その上で可能な他の試みは-

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

この設定により、カスケードは期待どおりに機能しました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.