SQLAlchemy: cascade supprimer

Je dois manquer quelque chose de trivial avec les options de cascade de SQLAlchemy parce que je ne peux pas obtenir une simple suppression de cascade 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):
    __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()

Sortie:

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 persister. Pourquoi? Comment puis-je faire supprimer les enfants en cascade?

62
demandé sur Vej 2011-02-17 22:22:54

6 réponses

Le problème est que sqlalchemy considère Child comme le 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 la relation sur la classe Parent à la place, cela fonctionnera:

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

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

Vous pouvez également ajouter delete-orphan (delete provoque des enfants être supprimés lorsque le parent est supprimé, delete-orphan supprime également tous les enfants qui ont été "enlevés" par les parents, même si le parent n'est pas supprimé)

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

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

(ce qui implique from sqlalchemy.orm import backref)

120
répondu Steven 2011-02-17 20:33:49

L'asnwer de@Steven est bon quand vous supprimez à travers 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). L'utilisation de cette méthode sqlalchemy's cascade='all, delete' ne fonctionne pas. Il existe cependant une solution: ON DELETE CASCADE Via db (note: toutes les bases de données ne le supportent pas).

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)
59
répondu Alex Okrushko 2012-10-09 13:52:11

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

TL; DR

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

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

Et un des relations suivantes:

A) ceci sur la table parent:

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

B) Ou ce sur l'enfant tableau:

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 relationship sur les tables parent ou enfant et cela fonctionnera bien. Bien que, apparemment sur les tables enfants, vous devez utiliser la fonction backref en plus de l'argument du 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é dans 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 de child_table seront supprimées pour vous par la base de données. Il est rapide et fiable et probablement votre meilleur pari. Vous configurez cela dans SqlAlchemy via ForeignKey comme ceci (partie de la table enfant la définition):

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 la ON DELETE CASCADE sur la table.

, Gotcha!

Il y a une mise en garde importante ici. Remarquez comment j'ai un relationship spécifié avec passive_deletes=True? Si vous n'avez pas cela, toute la chose 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 à NULL. Donc, si vous supprimez une ligne de parent_tableid = 5, alors il sera essentiellement exécuter

UPDATE child_table SET parent_id = NULL WHERE parent_id = 5

Pourquoi vous voulez cela, je ne sais pas. 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, en créant un orphelin. Semble être une mauvaise idée, mais peut-être qu'il y a 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 en utilisant le ON DELETE CASCADE que vous avez configuré. C'est parce qu'il s'appuie sur ces clés étrangères pour savoir quelles lignes enfants Supprimer. Une fois que SqlAlchemy les a tous définis à NULL, La base de données ne peut pas les supprimer. La définition de passive_deletes=True empêche SqlAlchemy de NULL extraire les clés étrangères.

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

Option 2

L'autre façon de le faire est de laisser SqlAlchemy le faire pour vous. Ceci est configuré en utilisant l'argument cascade 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 est sur l'enfant, vous faites comme ceci:

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

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

Ceci en place, lorsque vous supprimez une ligne parent, SqlAlchemy fonctionneront delete pour vous de nettoyer les lignes enfants. Cela ne sera probablement pas aussi efficace que de laisser cette base de données gérer si pour vous, donc je ne le recommande pas.

Voici les documents SqlAlchemy sur les fonctionnalités en cascade qu'il supporte.

43
répondu d512 2017-10-19 04:14:41

Steven est correct en ce sens que vous devez créer explicitement le backref, 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 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 lequel est le parent et qui est le enfant.

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

6
répondu Larry Weya 2013-04-21 13:57:55

J'ai aussi eu du mal avec la documentation, mais j'ai constaté que les docstrings eux-mêmes avaient tendance à être plus faciles que le manuel. Par exemple, si vous importez une relation à partir de sqlalchemy.orm et do help( relation), il vous donnera toutes les options que vous pouvez spécifier pour cascade. La puce pour "delete-orphan" dit: "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 parents présents."

Je me rends compte que votre problème était plus avec la façon dont la documentation pour définir les relations parent-enfant. Mais il semblait que vous pourriez aussi avoir un problème avec les options de cascade, car " all "inclut " delete".""supprimer orphelin" est la seule option qui n'est pas incluse dans "tout."

5
répondu Profane 2011-06-03 11:16:07

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 connaîtront pas ces contraintes et pourraient modifier vos données d'une manière qui brise le modèle de données logique que vous avez travaillé si dur pour concevoir.

Autant que possible, utilisez l'approche ForeignKey décrite par d512 et Alex. Le moteur DB est très bon pour appliquer réellement les contraintes (d'une 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 les versions de SQLite qui ne prennent pas en charge les clés étrangères.

Si vous avez besoin de créer un lien supplémentaire entre les entités pour activer les comportements d'application comme la navigation objet parent-enfant relations, utilisez backref en conjonction avec ForeignKey.

2
répondu Chris Johnson 2018-01-17 16:23:43