SQLAlchemy: Kaskadenlöschung

116

Bei den Kaskadenoptionen von SQLAlchemy muss mir etwas Triviales fehlen, da ich keine einfache Kaskadenlöschung erhalten kann, um ordnungsgemäß zu funktionieren. Wenn ein übergeordnetes Element gelöscht wird, bleiben die untergeordneten Elemente mit nullFremdschlüsseln erhalten.

Ich habe hier einen kurzen Testfall gestellt:

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

Ausgabe:

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

Es gibt eine einfache Eins-zu-Viele-Beziehung zwischen Eltern und Kind. Das Skript erstellt ein übergeordnetes Element, fügt 3 untergeordnete Elemente hinzu und schreibt dann fest. Als nächstes wird das übergeordnete Element gelöscht, die untergeordneten Elemente bleiben jedoch bestehen. Warum? Wie lösche ich die Kinderkaskade?

Carl
quelle
Dieser Abschnitt in den Dokumenten (zumindest jetzt, 3 Jahre später nach dem ursprünglichen Beitrag) scheint in diesem Zusammenhang sehr hilfreich zu sein: docs.sqlalchemy.org/en/rel_0_9/orm/session.html#cascades
Soferio

Antworten:

183

Das Problem ist, dass sqlalchemy Childals übergeordnetes Element betrachtet wird , da Sie dort Ihre Beziehung definiert haben (es ist natürlich egal, dass Sie sie "Kind" genannt haben).

Wenn Sie Parentstattdessen die Beziehung für die Klasse definieren , funktioniert dies:

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

(Hinweis "Child"als Zeichenfolge: Dies ist zulässig, wenn der deklarative Stil verwendet wird, damit Sie auf eine Klasse verweisen können, die noch nicht definiert ist.)

Möglicherweise möchten Sie auch hinzufügen delete-orphan( deletebewirkt, dass untergeordnete Elemente gelöscht werden, wenn das übergeordnete Element gelöscht wird, delete-orphanund löscht auch alle untergeordneten Elemente, die vom übergeordneten Element "entfernt" wurden, auch wenn das übergeordnete Element nicht gelöscht wird).

BEARBEITEN: gerade herausgefunden: Wenn Sie die Beziehung für die Klasse wirklich definieren möchten Child, können Sie dies tun, aber Sie müssen die Kaskade auf der Backref definieren (indem Sie die Backref explizit erstellen), wie folgt:

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

(impliziert from sqlalchemy.orm import backref)

Steven
quelle
6
Aha, das ist es. Ich wünschte, die Dokumentation wäre dazu expliziter!
Carl
15
Ja. Sehr hilfreich. Ich hatte immer Probleme mit der Dokumentation von SQLAlchemy.
Ayaz
1
Dies ist in der aktuellen doc docs.sqlalchemy.org/en/rel_0_9/orm/cascades.html
Epoc
1
@Lyman Zerga: in dem Beispiel des OP: Wenn Sie ein entfernen ChildObjekt aus parent.children, sollte das Objekt aus der Datenbank gelöscht werden, oder sollte es die nur als Referenz auf den übergeordneten entfernt werden (dh Satz. parentidSpalte null, stattdessen die Zeile zu löschen)
Steven
1
Warten Sie, das relationshipdiktiert nicht das Eltern-Kind-Setup. Die Verwendung ForeignKeyauf einem Tisch ist das, was ihn als Kind einrichtet. Es spielt keine Rolle, ob sich das relationshipauf dem Elternteil oder dem Kind befindet.
d512
110

@ Steven's Antwort ist gut, wenn Sie löschen session.delete(), was in meinem Fall nie passiert. Mir ist aufgefallen, dass ich die meiste Zeit durch lösche session.query().filter().delete()(wodurch keine Elemente in den Speicher gestellt und direkt aus der Datenbank gelöscht werden ). Mit dieser Methode funktioniert sqlalchemy's cascade='all, delete'nicht. Es gibt jedoch eine Lösung: ON DELETE CASCADEüber db (Hinweis: Nicht alle Datenbanken unterstützen dies).

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)
Alex Okrushko
quelle
3
Vielen Dank für die Erklärung dieses Unterschieds - ich habe versucht, session.query().filter().delete()das Problem zu nutzen und zu finden
nighthawk454
4
Ich musste festlegen passive_deletes='all', dass die untergeordneten Elemente von der Datenbankkaskade gelöscht werden, wenn das übergeordnete Element gelöscht wird. Mit passive_deletes=Truewurden untergeordnete Objekte getrennt (übergeordnetes Element auf NULL gesetzt), bevor das übergeordnete Objekt gelöscht wurde, sodass die Datenbankkaskade nichts unternahm.
Milorad Pop-Tosic
@ MiloradPop-Tosic Ich habe SQLAlchemy seit über 3 Jahren nicht mehr verwendet, aber das Lesen des Dokuments sieht aus wie passive_deletes = True ist immer noch das Richtige.
Alex Okrushko
2
Ich kann bestätigen, dass passive_deletes=Truedies in diesem Szenario ordnungsgemäß funktioniert.
d512
Ich hatte Probleme mit der automatischen Generierung von Alembic-Revisionen, die eine Kaskade beim Löschen beinhalteten - dies war die Antwort.
JNW
104

Ziemlich alter Beitrag, aber ich habe nur ein oder zwei Stunden damit verbracht, deshalb wollte ich meine Ergebnisse teilen, zumal einige der anderen aufgeführten Kommentare nicht ganz richtig sind.

TL; DR

Geben Sie der untergeordneten Tabelle eine Fremdtabelle oder ändern Sie die vorhandene und fügen Sie Folgendes hinzu ondelete='CASCADE':

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

Und eine der folgenden Beziehungen:

a) Dies in der übergeordneten Tabelle:

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

b) Oder dies auf dem Kindertisch:

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

Einzelheiten

Zunächst einmal wird die Eltern-Kind-Beziehung trotz der akzeptierten Antwort nicht durch Verwendung hergestellt relationship, sondern durch Verwendung ForeignKey. Sie können die relationshipentweder auf die übergeordnete oder die untergeordnete Tabelle setzen und es wird gut funktionieren. Obwohl Sie anscheinend in den untergeordneten Tabellen die backrefFunktion zusätzlich zum Schlüsselwortargument verwenden müssen.

Option 1 (bevorzugt)

Zweitens unterstützt SqlAlchemy zwei verschiedene Arten der Kaskadierung. Die erste und die von mir empfohlene ist in Ihre Datenbank integriert und hat normalerweise die Form einer Einschränkung für die Fremdschlüsseldeklaration. In PostgreSQL sieht es so aus:

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

Dies bedeutet, dass beim Löschen eines Datensatzes parent_tablealle entsprechenden Zeilen in child_tablevon der Datenbank für Sie gelöscht werden. Es ist schnell und zuverlässig und wahrscheinlich die beste Wahl. Sie richten dies in SqlAlchemy folgendermaßen ein ForeignKey(Teil der Definition der untergeordneten Tabelle):

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

Das ondelete='CASCADE'ist der Teil, der das ON DELETE CASCADEauf dem Tisch erstellt.

Erwischt!

Hier gibt es eine wichtige Einschränkung. Beachten Sie, wie ich eine mit relationshipangegeben habe passive_deletes=True? Wenn Sie das nicht haben, wird das Ganze nicht funktionieren. Dies liegt daran, dass SqlAlchemy beim Löschen eines übergeordneten Datensatzes standardmäßig etwas wirklich Seltsames tut. Es setzt die Fremdschlüssel aller untergeordneten Zeilen auf NULL. Wenn Sie also eine Zeile aus parent_tablewhere id= 5 löschen , wird sie grundsätzlich ausgeführt

UPDATE child_table SET parent_id = NULL WHERE parent_id = 5

Warum du das willst, weiß ich nicht. Es würde mich wundern, wenn Sie in vielen Datenbankmodulen sogar einen gültigen Fremdschlüssel festlegen könnten NULL, um eine Waise zu erstellen. Scheint eine schlechte Idee zu sein, aber vielleicht gibt es einen Anwendungsfall. Wenn Sie dies von SqlAlchemy ausführen lassen, verhindern Sie auf jeden Fall, dass die Datenbank die untergeordneten Elemente mit dem von ON DELETE CASCADEIhnen eingerichteten bereinigen kann. Dies liegt daran, dass diese Fremdschlüssel erforderlich sind, um zu wissen, welche untergeordneten Zeilen gelöscht werden sollen. Sobald SqlAlchemy sie alle festgelegt hat, NULLkann die Datenbank sie nicht mehr löschen. Durch Einstellen von wird passive_deletes=Trueverhindert, dass SqlAlchemy NULLdie Fremdschlüssel herausgibt .

Weitere Informationen zu passiven Löschvorgängen finden Sie in den SqlAlchemy-Dokumenten .

Option 2

Die andere Möglichkeit besteht darin, SqlAlchemy dies für Sie tun zu lassen. Dies wird mit dem cascadeArgument von eingerichtet relationship. Wenn Sie die Beziehung in der übergeordneten Tabelle definiert haben, sieht sie folgendermaßen aus:

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

Wenn die Beziehung auf dem Kind liegt, machen Sie es so:

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

Auch dies ist das untergeordnete Element. Sie müssen also eine aufgerufene Methode aufrufen backrefund die Kaskadendaten dort ablegen.

Wenn Sie eine übergeordnete Zeile löschen, führt SqlAlchemy tatsächlich Löschanweisungen aus, damit Sie die untergeordneten Zeilen bereinigen können. Dies ist wahrscheinlich nicht so effizient wie das Handhaben dieser Datenbank, wenn ich es für Sie nicht empfehle.

Hier sind die SqlAlchemy-Dokumente zu den unterstützten Kaskadenfunktionen .

d512
quelle
Danke für die Erklärung. Es macht jetzt Sinn.
Odin
1
Warum funktioniert es auch nicht, a Columnin der untergeordneten Tabelle als ForeignKey('parent.id', ondelete='cascade', onupdate='cascade')nicht deklariert zu deklarieren ? Ich habe erwartet, dass die Kinder gelöscht werden, wenn auch die übergeordnete Tabellenzeile gelöscht wird. Stattdessen setzt SQLA die untergeordneten Elemente entweder auf a parent.id=NULLoder belässt sie "wie sie sind", löscht sie jedoch nicht. Das ist nach der ursprünglichen Definition der relationshipim Elternteil als children = relationship('Parent', backref='parent')oder relationship('Parent', backref=backref('parent', passive_deletes=True)); DB zeigt cascadeRegeln in der DDL (SQLite3-basierter Proof-of-Concept). Gedanken?
code_dredd
1
Außerdem sollte ich beachten, dass ich bei der Verwendung backref=backref('parent', passive_deletes=True)die folgende Warnung erhalte: Dies SAWarning: On Parent.children, 'passive_deletes' is normally configured on one-to-many, one-to-one, many-to-many relationships only. "relationships only." % selfdeutet darauf hin, dass die Verwendung passive_deletes=Truein dieser (offensichtlichen) Eins-zu-Viele-Eltern-Kind-Beziehung aus irgendeinem Grund nicht gefällt .
code_dredd
Tolle Erklärung. Eine Frage - ist deleteüberflüssig in cascade='all,delete'?
Zaggi
1
@zaggi deleteist redundant in cascade='all,delete', da nach der SQLAlchemy der docs , allist ein Synonym für:save-update, merge, refresh-expire, expunge, delete
pmsoltani
7

Steven hat insofern Recht, als Sie die Backref explizit erstellen müssen. Dies führt dazu, dass die Kaskade auf die Eltern angewendet wird (im Gegensatz zu der, die wie im Testszenario auf das Kind angewendet wird).

Das Definieren der Beziehung für das Kind führt jedoch NICHT dazu, dass sqlalchemy Child als Elternteil betrachtet. Es spielt keine Rolle, wo die Beziehung definiert ist (Kind oder Elternteil), es ist der Fremdschlüssel, der die beiden Tabellen verbindet, die bestimmen, welches das Elternteil und welches das Kind ist.

Es ist jedoch sinnvoll, sich an eine Konvention zu halten, und basierend auf Stevens Antwort definiere ich alle meine Kinderbeziehungen für die Eltern.

Larry Weya
quelle
6

Ich hatte auch Probleme mit der Dokumentation, stellte jedoch fest, dass die Dokumentzeichenfolgen selbst in der Regel einfacher sind als das Handbuch. Wenn Sie beispielsweise eine Beziehung aus sqlalchemy.orm importieren und Hilfe (Beziehung) ausführen, erhalten Sie alle Optionen, die Sie für die Kaskade angeben können. Die Kugel für delete-orphansagt:

Wenn ein Element vom Typ des Kindes ohne übergeordnetes Element erkannt wird, markieren Sie es zum Löschen.
Beachten Sie, dass diese Option verhindert, dass ein ausstehendes Element der Klasse des Kindes beibehalten wird, ohne dass ein Elternteil anwesend ist.

Mir ist klar, dass Ihr Problem eher in der Dokumentation der Definition von Eltern-Kind-Beziehungen lag. Aber es schien, dass Sie auch ein Problem mit den Kaskadenoptionen haben könnten, weil "all"enthält "delete". "delete-orphan"ist die einzige Option, die nicht in enthalten ist "all".

Entweihen
quelle
Die Verwendung help(..)auf den sqlalchemyObjekten hilft sehr! Vielen Dank :-))) ! PyCharm zeigt im Kontext nichts an und hat offenbar vergessen, das zu überprüfen help. Vielen Dank!
dmitry_romanov
5

Stevens Antwort ist solide. Ich möchte auf eine zusätzliche Implikation hinweisen.

Durch die Nutzung relationship Sie die App-Ebene (Flask) für die referenzielle Integrität verantwortlich. Das bedeutet, dass andere Prozesse, die nicht über Flask auf die Datenbank zugreifen, wie ein Datenbankdienstprogramm oder eine Person, die eine direkte Verbindung zur Datenbank herstellt, diese Einschränkungen nicht erfahren und Ihre Daten so ändern können, dass das logische Datenmodell, an dem Sie so hart gearbeitet haben, beschädigt wird .

Verwenden Sie nach Möglichkeit den ForeignKeyvon d512 und Alex beschriebenen Ansatz. Die DB-Engine ist sehr gut darin, Einschränkungen wirklich (auf unvermeidbare Weise) durchzusetzen. Dies ist bei weitem die beste Strategie zur Aufrechterhaltung der Datenintegrität. Sie müssen sich nur dann auf eine App verlassen, um die Datenintegrität zu gewährleisten, wenn die Datenbank diese nicht verarbeiten kann, z. B. Versionen von SQLite, die keine Fremdschlüssel unterstützen.

Wenn Sie eine weitere Verknüpfung zwischen Entitäten erstellen müssen, um App-Verhalten wie das Navigieren in Eltern-Kind-Objektbeziehungen zu ermöglichen, verwenden Sie backrefin Verbindung mit ForeignKey.

Chris Johnson
quelle
2

Die Antwort von Stevan ist perfekt. Aber wenn Sie immer noch den Fehler bekommen. Ein anderer möglicher Versuch wäre -

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

Vom Link kopiert

Schneller Tipp, wenn Sie Probleme mit einer Fremdschlüsselabhängigkeit haben, auch wenn Sie in Ihren Modellen eine Kaskadenlöschung angegeben haben.

Verwenden Sie SQLAlchemy, um eine Kaskadenlöschung anzugeben, die Sie cascade='all, delete'in Ihrer übergeordneten Tabelle haben sollten. Ok, aber dann, wenn Sie etwas ausführen wie:

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

Es löst tatsächlich einen Fehler über einen Fremdschlüssel aus, der in Ihren untergeordneten Tabellen verwendet wird.

Die Lösung, mit der ich das Objekt abgefragt und dann gelöscht habe:

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)

Dies sollte Ihren übergeordneten Datensatz UND alle damit verbundenen untergeordneten Datensätze löschen.

Prashant Momale
quelle
1
Ist ein Anruf .first()erforderlich? Welche Filterbedingungen geben eine Liste von Objekten zurück und alles muss gelöscht werden? Erhält der Aufruf nicht .first()nur das erste Objekt? @ Prashant
Kavin Raju S
1

Die Antwort von Alex Okrushko hat für mich fast am besten funktioniert. Verwendet ondelete = 'CASCADE' und passive_deletes = True kombiniert. Aber ich musste etwas extra tun, damit es für SQLite funktioniert.

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)

Stellen Sie sicher, dass Sie diesen Code hinzufügen, um sicherzustellen, dass er für SQLite funktioniert.

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

Von hier gestohlen: SQLAlchemy-Ausdruckssprache und SQLites bei Löschkaskade

dummer Student
quelle
0

TLDR: Wenn die oben genannten Lösungen nicht funktionieren, fügen Sie Ihrer Spalte nullable = False hinzu.

Ich möchte hier einen kleinen Punkt für einige Leute hinzufügen, die möglicherweise nicht die Kaskadenfunktion erhalten, um mit den vorhandenen Lösungen zu arbeiten (die großartig sind). Der Hauptunterschied zwischen meiner Arbeit und dem Beispiel war, dass ich automap verwendet habe. Ich weiß nicht genau, wie dies die Einrichtung von Kaskaden beeinträchtigen könnte, aber ich möchte darauf hinweisen, dass ich es verwendet habe. Ich arbeite auch mit einer SQLite-Datenbank.

Ich habe jede hier beschriebene Lösung ausprobiert, aber für Zeilen in meiner untergeordneten Tabelle wurde der Fremdschlüssel beim Löschen der übergeordneten Zeile weiterhin auf null gesetzt. Ich hatte alle Lösungen hier ohne Erfolg ausprobiert. Die Kaskade funktionierte jedoch, nachdem ich die untergeordnete Spalte mit dem Fremdschlüssel auf nullable = False gesetzt hatte.

Auf dem Kindertisch fügte ich hinzu:

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

Bei diesem Setup funktionierte die Kaskade wie erwartet.

Spencer Weston
quelle