web-dev-qa-db-ja.com

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):
    __table= "parent"
    id = Column(Integer, primary_key = True)

class Child(Base):
    __table= "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つの子を追加してからコミットします。次に、親は削除されますが、子は保持されます。どうして?子をカスケード削除するにはどうすればよいですか?

83
carl

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

代わりにParentクラスでリレーションシップを定義すると、動作します:

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

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

delete-Orphanを追加することもできます(deleteは、親が削除されたときに子を削除します。delete-Orphanは、親から「削除」された子も削除します。親は削除されません)

編集:ちょうど見つけた:あなたが本当にChildクラスの関係を定義したいなら、あなたはそうすることができますが、あなたはそうします次のように、カスケードを定義する必要がありますbackrefで(明示的にbackrefを作成することにより):

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

from sqlalchemy.orm import backrefを意味する)

147
Steven

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

class Child(Base):
    __table= "children"

    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey("parents.id", ondelete='CASCADE'))

class Parent(Base):
    __table= "parents"

    id = Column(Integer, primary_key=True)
    child = relationship(Child, backref="parent", passive_deletes=True)
76
Alex Okrushko

かなり古い投稿ですが、これに1〜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)Orこれは子テーブルで:

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内の対応するすべての行がデータベースによって削除されます。高速で信頼性が高く、おそらく最善の方法です。次のように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=Truerelationshipが指定されていることに注目してください。あなたがそれを持っていない場合、全体が動作しません。これは、デフォルトで親レコードを削除すると、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

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

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

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

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

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

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

以下に、サポートするカスケード機能に関する SqlAlchemy docs を示します。

71
d512

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

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

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

7
Larry Weya

ドキュメントにも苦労しましたが、ドキュメント文字列自体はマニュアルよりも簡単な傾向があることがわかりました。たとえば、sqlalchemy.ormからリレーションシップをインポートしてhelp(relationship)を実行すると、カスケードに指定できるすべてのオプションが提供されます。 「delete-Orphan」の箇条書きには、「親のない子のタイプのアイテムが検出された場合、削除のマークを付けます。このオプションは、子のクラスの保留中のアイテムが親の存在なしで永続化されるのを防ぐことに注意してください」

あなたの問題は、親子関係を定義するためのドキュメントの方法に関するものであったことを理解しています。しかし、「すべて」には「削除」が含まれるため、カスケードオプションにも問題があるように思われます。 「孤児の削除」は、「すべて」に含まれていない唯一のオプションです。

5
Profane

スティーブンの答えは確かです。追加の意味を指摘したいと思います。

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

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

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

4
Chris Johnson

ステバンによる回答は完璧です。しかし、まだエラーが発生する場合。その上で可能な他の試みは-

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

リンクからコピー

モデルでカスケード削除を指定した場合でも、外部キーの依存関係で問題が発生した場合のクイックヒント。

SQLAlchemyを使用して、カスケード削除を指定するには、親テーブルでcascade = 'all、delete'が必要です。 Okただし、次のように実行すると:

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
Prashant Momale