web-dev-qa-db-fra.com

SQLAlchemy: suppression en cascade

Je dois manquer quelque chose de trivial avec les options de cascade de SQLAlchemy car je ne peux pas obtenir une suppression en cascade simple pour fonctionner correctement - si un élément parent est supprimé, les enfants persistent, avec null 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):
    __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()

Sortie:

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

Il existe une relation simple et 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 supprimer la cascade des enfants?

83
carl

Le problème est que sqlalchemy considère Child comme parent, car c'est là que vous avez défini votre relation (peu importe que vous l'appeliez "Enfant" bien sûr).

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

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

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

Vous voudrez peut-être ajouter delete-Orphan également (delete entraîne la suppression des enfants lorsque le parent est supprimé, delete-Orphan 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 classe Child, vous pouvez le faire, mais vous le ferez doivent définir la cascade sur la backref (en créant explicitement la backref), comme ceci:

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

(impliquant from sqlalchemy.orm import backref)

147
Steven

@ La réponse de Steven est bonne lorsque vous supprimez via 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 les éléments en mémoire et les supprime directement de db). En utilisant cette méthode sqlalchemy's cascade='all, delete' ne fonctionne pas. Il existe cependant une solution: ON DELETE CASCADE à db (remarque: toutes les bases de données ne le prennent pas en charge).

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

Assez vieux post, mais je viens de passer une heure ou deux là-dessus, donc je voulais partager ma conclusion, d'autant plus que certains des autres commentaires énumérés ne sont pas tout à fait corrects.

TL; DR

Donnez une table étrangère à la table enfant ou modifiez la table existante, en ajoutant ondelete='CASCADE':

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

Et 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 placer le relationship sur les tables parent ou enfant et cela fonctionnera correctement. Bien que, apparemment sur les tables enfants, vous devez utiliser la fonction backref 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. La première, et celle que je recommande, est intégrée à 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, puis toutes les lignes correspondantes dans child_table sera supprimé pour vous par la base de données. C'est rapide et fiable et probablement votre meilleur pari. Vous configurez cela dans SqlAlchemy via ForeignKey comme ceci (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 CASCADE sur la table.

Je t'ai eu!

Il y a une mise en garde importante ici. Remarquez comment j'ai un relationship spécifié avec passive_deletes=True? Si vous ne l'avez pas, le 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 de parent_tableid = 5, il s'exécutera

UPDATE child_table SET parent_id = NULL WHERE parent_id = 5

Pourquoi vous voudriez cela, 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 sur 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 le faire, vous empêcherez la base de données de pouvoir nettoyer les enfants à l'aide du ON DELETE CASCADE que vous avez configuré. En effet, il s'appuie sur ces clés étrangères pour savoir quelles lignes enfant supprimer. Une fois que SqlAlchemy les a tous définis sur NULL, la base de données ne peut pas les supprimer. Réglage du passive_deletes=True empêche SqlAlchemy de NULL d'extraire les clés étrangères.

Vous pouvez en savoir plus sur les suppressions passives dans le SqlAlchemy docs .

Option 2

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

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

Si la relation est sur l'enfant, vous le faites comme ceci:

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

Encore une fois, il s'agit de l'enfant, vous devez donc appeler une méthode appelée backref et y placer les données en cascade.

Avec cela en place, lorsque vous supprimez une ligne parent, SqlAlchemy exécute réellement les instructions de suppression pour que vous nettoyiez les lignes enfants. Ce ne sera probablement pas aussi efficace que de laisser cette base de données gérer si pour vous, je ne le recommande donc pas.

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

71
d512

Steven a raison en ce que vous devez créer explicitement le backref, ce qui entraîne l'application de la cascade sur le parent (par opposition à celle appliquée à l'enfant comme dans le scénario de test).

Cependant, définir la relation sur l'enfant ne fait PAS que sqlalchemy considère l'enfant 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 qui est le parent et qui est l'enfant.

Il est cependant logique de s'en tenir à une seule convention, et sur la base de la réponse de Steven, je définis toutes mes relations enfantines avec le parent.

7
Larry Weya

J'ai également eu du mal avec la documentation, mais j'ai constaté que les docstrings eux-mêmes ont tendance à être plus faciles que le manuel. Par exemple, si vous importez une relation à partir de sqlalchemy.orm et que vous aidez (relation), cela vous donnera toutes les options que vous pouvez spécifier pour la cascade. La puce pour "delete-Orphan" indique, "si un élément du type de l'enfant sans parent est détecté, marquez-le pour la suppression. Notez que cette option empêche la persistance d'un élément en attente de la classe de l'enfant sans la présence d'un parent."

Je me rends compte que votre problème était davantage lié à la façon dont la documentation pour définir les relations parent-enfant. Mais il semble que vous ayez également un problème avec les options de cascade, car "tout" inclut "supprimer". "supprimer orphelin" est la seule option qui n'est pas incluse dans "tous".

5
Profane

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 non via Flask, comme un utilitaire de base de données ou une personne se connectant directement à la base de données, ne rencontreront 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'approche ForeignKey décrite par d512 et Alex. Le moteur de base de données est très bon pour appliquer véritablement les contraintes (de manière inévitable), c'est donc de loin la meilleure stratégie pour maintenir l'intégrité des données. La seule fois où vous devez compter sur 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 versions de SQLite qui ne prennent pas en charge les clés étrangères.

Si vous devez 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 backref conjointement avec ForeignKey.

4
Chris Johnson

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, vous devez avoir cascade = 'all, delete' sur votre table parent. Ok mais 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 dossier parent ET tous les enfants qui lui sont associés.

1
Prashant Momale