SQLAlchemy: eliminazione a cascata


116

Devo mancare qualcosa di banale con le opzioni a cascata di SQLAlchemy perché non riesco a ottenere una semplice eliminazione a cascata per funzionare correttamente - se un elemento genitore è eliminato, i figli persistono, con nullchiavi esterne.

Ho messo qui un breve test case:

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

Produzione:

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

Esiste una semplice relazione uno-a-molti tra genitore e figlio. Lo script crea un genitore, aggiunge 3 figli, quindi esegue il commit. Successivamente, elimina il genitore, ma i figli persistono. Perché? Come faccio a eliminare i bambini a cascata?


Questa sezione nella documentazione (almeno ora, 3 anni dopo il post originale) sembra abbastanza utile su questo: docs.sqlalchemy.org/en/rel_0_9/orm/session.html#cascades
Soferio

Risposte:


184

Il problema è che sqlalchemy considera Childcome genitore, perché è lì che hai definito la tua relazione (non importa che tu l'abbia chiamata "Child" ovviamente).

Se invece definisci la relazione sulla Parentclasse, funzionerà:

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

(nota "Child"come stringa: questo è consentito quando si utilizza lo stile dichiarativo, in modo da poter fare riferimento a una classe che non è ancora definita)

Potresti anche voler aggiungere delete-orphan( deletefa sì che i figli vengano eliminati quando il genitore viene eliminato, delete-orphanelimina anche tutti i figli che sono stati "rimossi" dal genitore, anche se il genitore non viene eliminato)

EDIT: appena scoperto: se vuoi veramente definire la relazione sulla Childclasse, puoi farlo, ma dovrai definire la cascata sul backref (creando esplicitamente il backref), in questo modo:

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

(implicando from sqlalchemy.orm import backref)


6
Aha, è questo. Vorrei che la documentazione fosse più esplicita su questo!
carl

15
Sì. Molto utile. Ho sempre avuto problemi con la documentazione di SQLAlchemy.
ayaz

1
Questo è ben spiegato nell'attuale docs.sqlalchemy.org/en/rel_0_9/orm/cascades.html
Epoc

1
@Lyman Zerga: nell'esempio dell'OP: se rimuovi un Childoggetto da parent.children, dovrebbe quell'oggetto essere cancellato dal database, o dovrebbe essere rimosso solo il suo riferimento al genitore (es. Imposta la parentidcolonna su null, invece di eliminare la riga)
Steven

1
Aspetta, il relationshipnon detta la configurazione genitore-figlio. L'utilizzo ForeignKeysu un tavolo è ciò che lo imposta come bambino. Non importa se relationshipè sul genitore o sul figlio.
d512

110

@ La risposta di Steven è buona quando stai eliminando, session.delete()cosa che nel mio caso non accade mai. Ho notato che la maggior parte delle volte elimino tramite session.query().filter().delete()(che non inserisce elementi in memoria e cancella direttamente da db). L'utilizzo di questo metodo sqlalchemy's cascade='all, delete'non funziona. C'è una soluzione però: ON DELETE CASCADEtramite db (nota: non tutti i database lo supportano).

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)

3
Grazie per aver spiegato questa differenza - stavo cercando di utilizzare session.query().filter().delete()e lottando per trovare il problema
nighthawk454

4
Ho dovuto impostare passive_deletes='all'per far sì che i figli fossero eliminati dalla cascata del database quando il genitore viene eliminato. Con passive_deletes=True, gli oggetti figli venivano dissociati (genitore impostato su NULL) prima che il genitore fosse eliminato, quindi la cascata del database non stava facendo nulla.
Milorad Pop-Tosic

@ MiloradPop-Tosic Non uso SQLAlchemy da oltre 3 anni, ma leggere il documento sembra passive_deletes = True è ancora la cosa giusta.
Alex Okrushko

2
Posso confermare che passive_deletes=Truefunziona correttamente in questo scenario.
d512

Avevo problemi con le revisioni di generazione automatica dell'alambicco che includevano la cascata all'eliminazione: questa era la risposta.
JNW,

105

Post piuttosto vecchio, ma ho appena trascorso un'ora o due su questo, quindi volevo condividere la mia scoperta, soprattutto perché alcuni degli altri commenti elencati non sono del tutto corretti.

TL; DR

Dai alla tabella figlia una straniera o modifica quella esistente, aggiungendo ondelete='CASCADE':

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

E una delle seguenti relazioni:

a) Questo nella tabella genitore:

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

b) O questo sul tavolo del bambino:

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

Dettagli

Prima di tutto, nonostante quello che dice la risposta accettata, la relazione genitore / figlio non viene stabilita usando relationship, ma stabilita usando ForeignKey. Puoi metterlo relationshipsulla tabella padre o figlio e funzionerà bene. Sebbene, a quanto pare, nelle tabelle figlie, devi usare la backreffunzione oltre all'argomento della parola chiave.

Opzione 1 (preferita)

In secondo luogo, SqlAlchemy supporta due diversi tipi di cascata. Il primo, e quello che consiglio, è integrato nel database e di solito assume la forma di un vincolo sulla dichiarazione di chiave esterna. In PostgreSQL assomiglia a questo:

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

Ciò significa che quando elimini un record da parent_table, tutte le righe corrispondenti child_tableverranno eliminate per te dal database. È veloce e affidabile e probabilmente è la soluzione migliore. Lo imposti in SqlAlchemy in ForeignKeyquesto modo (parte della definizione della tabella figlio):

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

Il ondelete='CASCADE'è la parte che crea la ON DELETE CASCADEsul tavolo.

Gotcha!

C'è un avvertimento importante qui. Notare come ho relationshipspecificato con passive_deletes=True? Se non ce l'hai, l'intera cosa non funzionerà. Questo perché per impostazione predefinita, quando elimini un record principale, SqlAlchemy fa qualcosa di veramente strano. Imposta le chiavi esterne di tutte le righe figlie su NULL. Quindi, se elimini una riga da parent_tabledove id= 5, fondamentalmente verrà eseguita

UPDATE child_table SET parent_id = NULL WHERE parent_id = 5

Perché vorresti questo non ne ho idea. Sarei sorpreso se molti motori di database consentissero anche di impostare una chiave esterna valida NULL, creando un orfano. Sembra una cattiva idea, ma forse c'è un caso d'uso. Ad ogni modo, se lasci fare a SqlAlchemy, impedirai al database di essere in grado di ripulire i figli usando il ON DELETE CASCADEche hai impostato. Questo perché si basa su quelle chiavi esterne per sapere quali righe figlie eliminare. Una volta che SqlAlchemy li ha impostati tutti su NULL, il database non può eliminarli. L'impostazione di passive_deletes=Trueimpedisce a SqlAlchemy di NULLestrarre le chiavi esterne.

Puoi leggere ulteriori informazioni sulle eliminazioni passive nei documenti di SqlAlchemy .

opzione 2

L'altro modo in cui puoi farlo è lasciare che SqlAlchemy lo faccia per te. Questo viene impostato utilizzando l' cascadeargomento di relationship. Se hai la relazione definita nella tabella genitore, è simile a questa:

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

Se la relazione è sul bambino, lo fai in questo modo:

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

Di nuovo, questo è il bambino, quindi devi chiamare un metodo chiamato backrefe inserire i dati a cascata lì.

Con questa impostazione, quando elimini una riga padre, SqlAlchemy eseguirà effettivamente le istruzioni di eliminazione per ripulire le righe figlio. Questo probabilmente non sarà efficiente come lasciare che questo database gestisca se per te, quindi non lo consiglio.

Di seguito sono riportati i documenti di SqlAlchemy sulle funzionalità a cascata che supporta.


Grazie per la spiegazione. Adesso ha senso.
Odino

1
Perché neanche dichiarare a Columnnella tabella figlia come ForeignKey('parent.id', ondelete='cascade', onupdate='cascade')non funziona? Mi aspettavo che i bambini venissero eliminati quando anche la riga della tabella padre veniva eliminata. Invece, SQLA imposta i figli su a parent.id=NULLo li lascia "così come sono", ma non elimina. Questo è dopo aver originariamente definito il relationshipnel genitore come children = relationship('Parent', backref='parent')o relationship('Parent', backref=backref('parent', passive_deletes=True)); DB mostra le cascaderegole nel DDL (proof-of-concept basato su SQLite3). Pensieri?
code_dredd

1
Inoltre, dovrei notare che quando lo uso backref=backref('parent', passive_deletes=True)ricevo il seguente avviso:, SAWarning: On Parent.children, 'passive_deletes' is normally configured on one-to-many, one-to-one, many-to-many relationships only. "relationships only." % selfsuggerendo che non gli piace l'uso di passive_deletes=Truein questa (ovvia) relazione uno-a-molti genitore-figlio per qualche motivo.
code_dredd

Ottima spiegazione. Una domanda: è deleteridondante in cascade='all,delete'?
zaggi

1
@zaggi deleteè ridondante in cascade='all,delete', dal momento che secondo la documentazione del SQLAlchemy , allè sinonimo di:save-update, merge, refresh-expire, expunge, delete
pmsoltani

7

Steven ha ragione in quanto è necessario creare esplicitamente il backref, questo si traduce nell'applicazione della cascata sul genitore (invece di applicarla al bambino come nello scenario del test).

Tuttavia, la definizione della relazione su Child NON fa sì che sqlalchemy consideri Child il genitore. Non importa dove sia definita la relazione (figlio o genitore), è la chiave esterna che collega le due tabelle che determina quale è il genitore e quale è il figlio.

Tuttavia, ha senso attenersi a una convenzione e, in base alla risposta di Steven, sto definendo tutti i miei rapporti tra figli e genitori.


6

Ho faticato anche con la documentazione, ma ho scoperto che le docstring stesse tendono ad essere più facili del manuale. Ad esempio, se importi la relazione da sqlalchemy.orm e fai help (relazione), ti verranno fornite tutte le opzioni che puoi specificare per la cascata. Il proiettile per delete-orphandice:

se viene rilevato un elemento del tipo del bambino senza genitore, contrassegnarlo per l'eliminazione.
Notare che questa opzione impedisce che un elemento in sospeso della classe del bambino venga mantenuto senza la presenza di un genitore.

Mi rendo conto che il tuo problema riguardava più il modo in cui la documentazione per definire le relazioni genitore-figlio. Ma sembrava che potresti anche avere un problema con le opzioni a cascata, perché "all"include "delete". "delete-orphan"è l'unica opzione non inclusa in "all".


Usare help(..)sugli sqlalchemyoggetti aiuta molto! Grazie :-))) ! PyCharm non mostra nulla nei dock contestuali e si è chiaramente dimenticato di controllare il file help. Grazie mille!
dmitry_romanov

5

La risposta di Steven è solida. Vorrei sottolineare un'ulteriore implicazione.

Usando relationship, stai rendendo il livello dell'app (Flask) responsabile dell'integrità referenziale. Ciò significa che altri processi che accedono al database non tramite Flask, come un'utilità di database o una persona che si connette direttamente al database, non sperimenteranno questi vincoli e potrebbero modificare i dati in un modo che interrompe il modello logico di dati per cui hai lavorato così duramente per progettare .

Quando possibile, usa l' ForeignKeyapproccio descritto da d512 e Alex. Il motore DB è molto bravo a far rispettare veramente i vincoli (in un modo inevitabile), quindi questa è di gran lunga la migliore strategia per mantenere l'integrità dei dati. L'unica volta in cui è necessario fare affidamento su un'app per gestire l'integrità dei dati è quando il database non è in grado di gestirli, ad esempio le versioni di SQLite che non supportano le chiavi esterne.

Se è necessario creare un ulteriore collegamento tra le entità per abilitare comportamenti delle app come la navigazione delle relazioni tra oggetti padre e figlio, utilizzare backrefinsieme a ForeignKey.


2

La risposta di Stevan è perfetta. Ma se ricevi ancora l'errore. Un altro possibile tentativo oltre a quello sarebbe:

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

Copiato dal link-

Suggerimento rapido se hai problemi con una dipendenza da chiave esterna anche se hai specificato un'eliminazione a cascata nei tuoi modelli.

Utilizzando SQLAlchemy, per specificare un'eliminazione a cascata che dovresti avere cascade='all, delete'sulla tua tabella genitore. Ok ma poi quando esegui qualcosa come:

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

In realtà attiva un errore su una chiave esterna utilizzata nelle tabelle figlie.

La soluzione l'ho usata per interrogare l'oggetto e poi cancellarlo:

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)

Questo dovrebbe eliminare il tuo record genitore E tutti i figli ad esso associati.


1
È .first()richiesta la chiamata ? Quali condizioni di filtro restituiscono un elenco di oggetti e tutto deve essere cancellato? La chiamata non .first()ottiene solo il primo oggetto? @Prashant
Kavin Raju S

2

La risposta di Alex Okrushko ha quasi funzionato meglio per me. Usati ondelete = 'CASCADE' e passive_deletes = True combinati. Ma ho dovuto fare qualcosa in più per farlo funzionare per 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)

Assicurati di aggiungere questo codice per assicurarti che funzioni per 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()

Rubato da qui: linguaggio delle espressioni SQLAlchemy e SQLite in eliminazione a cascata


0

TLDR: se le soluzioni precedenti non funzionano, prova ad aggiungere nullable = False alla tua colonna.

Vorrei aggiungere un piccolo punto qui per alcune persone che potrebbero non far funzionare la funzione a cascata con le soluzioni esistenti (che sono fantastiche). La principale differenza tra il mio lavoro e l'esempio è che ho usato automap. Non so esattamente come possa interferire con la configurazione delle cascate, ma voglio sottolineare che l'ho usato. Sto anche lavorando con un database SQLite.

Ho provato tutte le soluzioni descritte qui, ma le righe nella mia tabella figlia hanno continuato ad avere la loro chiave esterna impostata su null quando la riga padre è stata eliminata. Avevo provato tutte le soluzioni qui senza alcun risultato. Tuttavia, la cascata ha funzionato una volta che ho impostato la colonna figlia con la chiave esterna su nullable = False.

Sul tavolo del bambino, ho aggiunto:

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

Con questa configurazione, la cascata ha funzionato come previsto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.