SQLAlchemy: suppression en cascade

116

Je dois manquer quelque chose de trivial avec les options en cascade de SQLAlchemy car je ne peux pas obtenir une simple suppression en cascade pour fonctionner correctement - si un élément parent est supprimé, les enfants persistent, avec nulldes clés étrangères.

J'ai mis un cas de test concis ici:

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

Production:

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

Il existe une relation simple, un-à-plusieurs entre le parent et l'enfant. Le script crée un parent, ajoute 3 enfants, puis valide. Ensuite, il supprime le parent, mais les enfants persistent. Pourquoi? Comment faire supprimer les enfants en cascade?

Carl
la source
Cette section de la documentation (au moins maintenant, 3 ans plus tard après le message d'origine) semble très utile à ce sujet: docs.sqlalchemy.org/en/rel_0_9/orm/session.html#cascades
Soferio

Réponses:

184

Le problème est que sqlalchemy considère Childcomme le parent, car c'est là que vous avez défini votre relation (il ne se soucie pas que vous l' appeliez "Enfant" bien sûr).

Si vous définissez Parentplutôt la relation sur la classe, cela fonctionnera:

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

(note "Child"sous forme de chaîne: ceci est autorisé lors de l'utilisation du style déclaratif, afin que vous puissiez faire référence à une classe qui n'est pas encore définie)

Vous voudrez peut-être également ajouter delete-orphan( deleteentraîne la delete-orphansuppression des enfants lorsque le parent est supprimé, supprime également tous les enfants qui ont été "supprimés" du parent, même si le parent n'est pas supprimé)

EDIT: vient de découvrir: si vous voulez vraiment définir la relation sur la Childclasse, vous pouvez le faire, mais vous devrez définir la cascade sur la backref (en créant explicitement la backref), comme ceci:

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

(sous-entendu from sqlalchemy.orm import backref)

Steven
la source
6
Aha, c'est ça. J'aurais aimé que la documentation soit plus explicite à ce sujet!
carl
15
Toujours. Très utile. J'ai toujours eu des problèmes avec la documentation de SQLAlchemy.
ayaz
1
Ceci est bien expliqué dans le doc actuel docs.sqlalchemy.org/en/rel_0_9/orm/cascades.html
Epoc
1
@Lyman Zerga: dans l'exemple de l'OP: si vous supprimez un Childobjet de parent.children, cet objet doit-il être supprimé de la base de données, ou doit-il seulement supprimer sa référence au parent (c'est-à-dire définir la parentidcolonne sur null, au lieu de supprimer la ligne)
Steven
1
Attendez, le relationshipne dicte pas la configuration parent-enfant. Utiliser ForeignKeysur une table est ce qui le définit comme l'enfant. Peu importe si relationshipc'est le parent ou l'enfant.
d512
110

La réponse de @ Steven est bonne lorsque vous supprimez session.delete()ce qui n'arrive jamais dans mon cas. J'ai remarqué que la plupart du temps, je supprime via session.query().filter().delete()(qui ne met pas d'éléments dans la mémoire et supprime directement de db). En utilisant cette méthode, sqlalchemy cascade='all, delete'ne fonctionne pas. Il existe cependant une solution: ON DELETE CASCADEvia db (note: toutes les bases de données ne le prennent pas en charge).

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
la source
3
Merci d'avoir expliqué cette différence - j'essayais d'utiliser session.query().filter().delete()et j'ai du mal à trouver le problème
nighthawk454
4
J'ai dû définir passive_deletes='all'pour que les enfants soient supprimés par la cascade de bases de données lorsque le parent est supprimé. Avec passive_deletes=True, les objets enfants étaient dissociés (le parent défini sur NULL) avant la suppression du parent, de sorte que la cascade de bases de données ne faisait rien.
Milorad Pop-Tosic
@ MiloradPop-Tosic Je n'ai pas utilisé SQLAlchemy depuis plus de 3 ans mais la lecture de la documentation ressemble à passive_deletes = True est toujours la bonne chose.
Alex Okrushko
2
Je peux confirmer que passive_deletes=Truecela fonctionne correctement dans ce scénario.
d512
J'avais des problèmes avec les révisions générées automatiquement par alambic qui incluaient la suppression en cascade - c'était la réponse.
JNW
105

Un article assez ancien, mais je viens de passer une heure ou deux à ce sujet, alors je voulais partager ma découverte, d'autant plus que certains des autres commentaires énumérés ne sont pas tout à fait corrects.

TL; DR

Donnez à la table enfant un étranger ou modifiez l'existant, en ajoutant ondelete='CASCADE':

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

Et l' une des relations suivantes:

a) Ceci sur la table parent:

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

b) Ou ceci sur la table enfant:

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

Détails

Tout d'abord, malgré ce que dit la réponse acceptée, la relation parent / enfant n'est pas établie en utilisant relationship, elle est établie en utilisant ForeignKey. Vous pouvez mettre le relationshipsur les tables parent ou enfant et cela fonctionnera très bien. Bien que, apparemment sur les tables enfants, vous devez utiliser la backreffonction en plus de l'argument mot-clé.

Option 1 (préférée)

Deuxièmement, SqlAlchemy prend en charge deux types différents de cascade. Le premier, et celui que je recommande, est intégré à votre base de données et prend généralement la forme d'une contrainte sur la déclaration de clé étrangère. Dans PostgreSQL, cela ressemble à ceci:

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

Cela signifie que lorsque vous supprimez un enregistrement de parent_table, toutes les lignes correspondantes dans child_tableseront supprimées pour vous par la base de données. C'est rapide et fiable et probablement votre meilleur pari. Vous configurez cela dans SqlAlchemy ForeignKeycomme ceci (une partie de la définition de la table enfant):

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

Le ondelete='CASCADE'est la partie qui crée le ON DELETE CASCADEsur la table.

Je t'ai eu!

Il y a une mise en garde importante ici. Remarquez comment j'ai relationshipspécifié avec passive_deletes=True? Si vous ne l'avez pas, tout ne fonctionnera pas. En effet, par défaut, lorsque vous supprimez un enregistrement parent, SqlAlchemy fait quelque chose de vraiment bizarre. Il définit les clés étrangères de toutes les lignes enfants sur NULL. Donc, si vous supprimez une ligne d' parent_tableid= 5, alors il exécutera essentiellement

UPDATE child_table SET parent_id = NULL WHERE parent_id = 5

Pourquoi tu voudrais ça, je n'en ai aucune idée. Je serais surpris si de nombreux moteurs de base de données vous permettaient même de définir une clé étrangère valide NULL, créant ainsi un orphelin. Cela semble être une mauvaise idée, mais il y a peut-être un cas d'utilisation. Quoi qu'il en soit, si vous laissez SqlAlchemy faire cela, vous empêcherez la base de données de pouvoir nettoyer les enfants à l'aide du fichier ON DELETE CASCADEque vous avez configuré. En effet, il s'appuie sur ces clés étrangères pour savoir quelles lignes enfants supprimer. Une fois que SqlAlchemy les a tous définis sur NULL, la base de données ne peut pas les supprimer. La définition de passive_deletes=Trueempêche SqlAlchemy d'entrer NULLles clés étrangères.

Vous pouvez en savoir plus sur les suppressions passives dans la documentation SqlAlchemy .

Option 2

L'autre façon dont vous pouvez le faire est de laisser SqlAlchemy le faire pour vous. Ceci est configuré à l'aide de l' cascadeargument du relationship. Si vous avez la relation définie sur la table parent, cela ressemble à ceci:

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

Si la relation concerne l'enfant, procédez comme suit:

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

Encore une fois, c'est l'enfant, vous devez donc appeler une méthode appelée backrefet y placer les données en cascade.

Avec cela en place, lorsque vous supprimez une ligne parent, SqlAlchemy exécutera en fait des instructions de suppression pour vous permettre de nettoyer les lignes enfants. Ce ne sera probablement pas aussi efficace que de laisser cette base de données gérer si c'est pour vous, donc je ne le recommande pas.

Voici la documentation SqlAlchemy sur les fonctionnalités en cascade qu'il prend en charge.

d512
la source
Merci pour votre explication. Cela a du sens maintenant.
Odin
1
Pourquoi déclarer a Columndans la table enfant comme ForeignKey('parent.id', ondelete='cascade', onupdate='cascade')ne fonctionne pas non plus? Je m'attendais à ce que les enfants soient supprimés lorsque leur ligne de table parent a également été supprimée. Au lieu de cela, SQLA définit les enfants sur a parent.id=NULLou les laisse "tels quels ", mais aucune suppression. C'est après avoir défini à l'origine le relationshipdans le parent comme children = relationship('Parent', backref='parent')ou relationship('Parent', backref=backref('parent', passive_deletes=True)); DB affiche les cascaderègles dans le DDL (preuve de concept basée sur SQLite3). Pensées?
code_dredd
1
De plus, je dois noter que lorsque j'utilise, backref=backref('parent', passive_deletes=True)je reçois l'avertissement suivant:, SAWarning: On Parent.children, 'passive_deletes' is normally configured on one-to-many, one-to-one, many-to-many relationships only. "relationships only." % selfsuggérant qu'il n'aime pas l'utilisation de passive_deletes=Truedans cette relation parent-enfant (évidente) un-à-plusieurs pour une raison quelconque.
code_dredd
Excellente explication. Une question - est deleteredondant cascade='all,delete'?
zaggi
1
@zaggi deleteEST redondant dans cascade='all,delete', puisque selon la documentation de SQLAlchemy , allest synonyme de:save-update, merge, refresh-expire, expunge, delete
pmsoltani
7

Steven a raison en ce sens que vous devez créer explicitement la référence arrière, ce qui entraîne l'application de la cascade sur le parent (par opposition à son application à l'enfant comme dans le scénario de test).

Cependant, la définition de la relation sur Child ne fait PAS que sqlalchemy considère Child comme le parent. Peu importe où la relation est définie (enfant ou parent), c'est la clé étrangère qui relie les deux tables qui détermine lequel est le parent et lequel est l'enfant.

Cependant, il est logique de s'en tenir à une convention et, sur la base de la réponse de Steven, je définis toutes les relations de mon enfant sur le parent.

Larry Weya
la source
6

J'ai également eu du mal avec la documentation, mais j'ai trouvé que les docstrings eux-mêmes ont tendance à être plus faciles que le manuel. Par exemple, si vous importez une relation depuis sqlalchemy.orm et que vous faites de l'aide (relation), cela vous donnera toutes les options que vous pouvez spécifier pour cascade. La balle pour delete-orphandit:

si un élément du type enfant sans parent est détecté, marquez-le pour suppression.
Notez que cette option empêche un élément en attente de la classe de l'enfant d'être conservé sans la présence d'un parent.

Je sais que votre problème concernait davantage la manière dont la documentation pour définir les relations parents-enfants. Mais il semble que vous ayez peut-être également un problème avec les options en cascade, car "all"comprend "delete". "delete-orphan"est la seule option non incluse dans "all".

Profane
la source
Utiliser help(..)les sqlalchemyobjets aide beaucoup! Merci :-))) ! PyCharm n'affiche rien dans les docks contextuels et a clairement oublié de vérifier le fichier help. Merci beaucoup!
dmitry_romanov
5

La réponse de Steven est solide. Je voudrais souligner une implication supplémentaire.

En utilisant relationship, vous rendez la couche d'application (Flask) responsable de l'intégrité référentielle. Cela signifie que d'autres processus qui accèdent à la base de données sans passer par Flask, comme un utilitaire de base de données ou une personne se connectant directement à la base de données, ne subiront pas ces contraintes et pourraient modifier vos données d'une manière qui rompt le modèle de données logique que vous avez travaillé si dur à concevoir. .

Dans la mesure du possible, utilisez l' ForeignKeyapproche décrite par d512 et Alex. Le moteur de base de données est très efficace pour vraiment appliquer les contraintes (de manière inévitable), c'est donc de loin la meilleure stratégie pour maintenir l'intégrité des données. Le seul moment où vous devez vous fier à une application pour gérer l'intégrité des données est lorsque la base de données ne peut pas les gérer, par exemple les versions de SQLite qui ne prennent pas en charge les clés étrangères.

Si vous avez besoin de créer davantage de liens entre les entités pour activer des comportements d'application tels que la navigation dans les relations d'objet parent-enfant, utilisez-le backrefconjointement avec ForeignKey.

Chris Johnson
la source
2

La réponse de Stevan est parfaite. Mais si vous obtenez toujours l'erreur. Un autre essai possible en plus de cela serait -

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

Copié à partir du lien-

Astuce rapide si vous rencontrez des problèmes avec une dépendance de clé étrangère même si vous avez spécifié une suppression en cascade dans vos modèles.

En utilisant SQLAlchemy, pour spécifier une suppression en cascade que vous devez avoir cascade='all, delete'sur votre table parent. Ok mais alors quand vous exécutez quelque chose comme:

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

Il déclenche en fait une erreur sur une clé étrangère utilisée dans vos tables enfants.

La solution que je l'ai utilisée pour interroger l'objet puis le supprimer:

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)

Cela devrait supprimer votre enregistrement parent ET tous les enfants qui lui sont associés.

Prashant Momale
la source
1
Un appel est-il .first()nécessaire? Quelles conditions de filtrage renvoient une liste d'objets et tout doit être supprimé? L'appel .first()n'obtient -il pas seulement le premier objet? @Prashant
Kavin Raju S
2

La réponse d'Alex Okrushko a presque fonctionné le mieux pour moi. Utilisé ondelete = 'CASCADE' et passive_deletes = True combinés. Mais j'ai dû faire quelque chose de plus pour que cela fonctionne pour 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)

Assurez-vous d'ajouter ce code pour vous assurer qu'il fonctionne pour 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()

Volé d'ici: langage d'expression SQLAlchemy et SQLite sur la cascade de suppression

stupide étudiant
la source
0

TLDR: Si les solutions ci-dessus ne fonctionnent pas, essayez d'ajouter nullable = False à votre colonne.

Je voudrais ajouter un petit point ici pour certaines personnes qui ne peuvent pas faire fonctionner la fonction de cascade avec les solutions existantes (qui sont excellentes). La principale différence entre mon travail et l'exemple était que j'utilisais automap. Je ne sais pas exactement comment cela pourrait interférer avec la configuration des cascades, mais je tiens à noter que je l'ai utilisé. Je travaille également avec une base de données SQLite.

J'ai essayé toutes les solutions décrites ici, mais les lignes de ma table enfant ont continué à avoir leur clé étrangère définie sur null lorsque la ligne parent a été supprimée. J'avais essayé toutes les solutions ici en vain. Cependant, la cascade a fonctionné une fois que j'ai défini la colonne enfant avec la clé étrangère sur nullable = False.

Sur la table enfant, j'ai ajouté:

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

Avec cette configuration, la cascade a fonctionné comme prévu.

Spencer Weston
la source