SQLAlchemy: eliminación en cascada

116

Debo estar perdiendo algo trivial con las opciones en cascada de SQLAlchemy porque no puedo hacer que una simple eliminación en cascada funcione correctamente: si un elemento principal se elimina, los elementos secundarios persisten, con nullclaves externas.

He puesto un caso de prueba conciso aquí:

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

Salida:

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

Existe una relación simple de uno a muchos entre padre e hijo. El script crea un padre, agrega 3 hijos y luego confirma. A continuación, elimina el padre, pero los hijos persisten. ¿Por qué? ¿Cómo hago que los niños se eliminen en cascada?

carl
fuente
Esta sección en los documentos (al menos ahora, 3 años después de la publicación original) parece bastante útil en esto: docs.sqlalchemy.org/en/rel_0_9/orm/session.html#cascades
Soferio

Respuestas:

184

El problema es que sqlalchemy considera Childcomo padre, porque ahí es donde definiste tu relación (no le importa que la hayas llamado "Niño" por supuesto).

Si define la relación en la Parentclase en su lugar, funcionará:

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

(nota "Child"como una cadena: esto está permitido cuando se usa el estilo declarativo, para que pueda hacer referencia a una clase que aún no está definida)

Es posible que también desee agregar delete-orphan( deletehace que los niños se eliminen cuando se elimina el padre, delete-orphantambién elimina los hijos que se "eliminaron" del padre, incluso si el padre no se elimina)

EDITAR: acabo de descubrir: si realmente desea definir la relación en la Childclase, puede hacerlo, pero tendrá que definir la cascada en el backref (creando el backref explícitamente), así:

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

(implicando from sqlalchemy.orm import backref)

Steven
fuente
6
Ajá, esto es todo. ¡Ojalá la documentación fuera más explícita sobre esto!
carl
15
Sí. Muy útil. Siempre he tenido problemas con la documentación de SQLAlchemy.
ayaz
1
Esto está bien explicado en el documento actual docs.sqlalchemy.org/en/rel_0_9/orm/cascades.html
Epoc
1
@Lyman Zerga: en el ejemplo de la OP: si se elimina un Childobjeto a partir de parent.children, debe suprimirse ese objeto de la base de datos, o sólo debe el mismo de la referencia a la matriz puede quitar (es decir, conjunto. parentidColumna a null, en lugar de eliminar la fila)
Steven
1
Espere, relationshipno dicta la configuración padre-hijo. Usarlo ForeignKeyen una mesa es lo que lo configura como niño. No importa si relationshipes del padre o del hijo.
d512
110

La respuesta de @ Steven es buena cuando estás eliminando, lo session.delete()que nunca sucede en mi caso. Me di cuenta de que la mayoría de las veces elimino a través session.query().filter().delete()(lo que no coloca elementos en la memoria y elimina directamente de la base de datos). Usar este método sqlalchemy's cascade='all, delete'no funciona. Sin embargo, hay una solución: a ON DELETE CASCADEtravés de db (nota: no todas las bases de datos lo admiten).

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
fuente
3
Gracias por explicar esta diferencia - estaba tratando de usar session.query().filter().delete()y luchando por encontrar el problema
nighthawk454
4
Tuve que configurar passive_deletes='all'para que los niños fueran eliminados por la cascada de la base de datos cuando se elimina el padre. Con passive_deletes=True, los objetos secundarios se disociaron (el padre se estableció en NULL) antes de que se eliminara el padre, por lo que la cascada de la base de datos no estaba haciendo nada.
Milorad Pop-Tosic
@ MiloradPop-Tosic No he usado SQLAlchemy durante más de 3 años, pero leer el documento parece que passive_deletes = True sigue siendo lo correcto.
Alex Okrushko
2
Puedo confirmar que passive_deletes=Truefunciona correctamente en este escenario.
d512
Estaba teniendo problemas con las revisiones de generación automática de alambique que incluían cascada al eliminar; esta fue la respuesta.
JNW
105

Una publicación bastante antigua, pero acabo de pasar una hora o dos en esto, así que quería compartir mi hallazgo, especialmente porque algunos de los otros comentarios enumerados no son del todo correctos.

TL; DR

Dele a la tabla secundaria una ajena o modifique la existente, agregando ondelete='CASCADE':

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

Y una de las siguientes relaciones:

a) Esto en la mesa principal:

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

b) O esto en la mesa del niño:

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

Detalles

En primer lugar, a pesar de lo que dice la respuesta aceptada, la relación padre / hijo no se establece mediante el uso de relationship , se establece mediante el uso ForeignKey. Puede poner el relationshipen la tabla principal o secundaria y funcionará bien. Aunque, aparentemente en las tablas secundarias, debe usar la backreffunción además del argumento de palabra clave.

Opción 1 (preferida)

En segundo lugar, SqlAlchemy admite dos tipos diferentes de conexión en cascada. El primero, y el que recomiendo, está integrado en su base de datos y generalmente toma la forma de una restricción en la declaración de clave externa. En PostgreSQL se ve así:

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

Esto significa que cuando elimine un registro de parent_table, child_tablela base de datos eliminará todas las filas correspondientes . Es rápido y confiable y probablemente su mejor opción. Configuraste esto en SqlAlchemy a través deForeignKey esta manera (parte de la definición de la tabla secundaria):

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

los ondelete='CASCADE' es la parte que crea el ON DELETE CASCADEsobre la mesa.

¡Te tengo!

Aquí hay una advertencia importante. ¿Observa cómo he relationshipespecificado con passive_deletes=True? Si no tiene eso, todo no funcionará. Esto se debe a que, de forma predeterminada, cuando elimina un registro principal, SqlAlchemy hace algo realmente extraño. Establece las claves externas de todas las filas secundarias en NULL. Entonces, si elimina una fila de parent_tabledonde id= 5, básicamente se ejecutará

UPDATE child_table SET parent_id = NULL WHERE parent_id = 5

Por qué querrías esto, no tengo idea. Me sorprendería si muchos motores de bases de datos incluso le permitieran establecer una clave externa válida para NULLcrear un huérfano. Parece una mala idea, pero tal vez haya un caso de uso. De todos modos, si deja que SqlAlchemy haga esto, evitará que la base de datos pueda limpiar a los niños usando el archivo ON DELETE CASCADEque configuró. Esto se debe a que se basa en esas claves externas para saber qué filas secundarias eliminar. Una vez que SqlAlchemy los ha configurado a todos NULL, la base de datos no puede eliminarlos. Establecer elpassive_deletes=True evita que SqlAlchemy extraiga NULLlas claves externas.

Puede leer más sobre eliminaciones pasivas en el documentos de SqlAlchemy .

opcion 2

La otra forma en que puede hacerlo es dejar que SqlAlchemy lo haga por usted. Esto se configura utilizando el cascadeargumento de relationship. Si tiene la relación definida en la tabla principal, se ve así:

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

Si la relación es del niño, hazlo así:

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

Nuevamente, este es el hijo, por lo que debe llamar a un método llamado backrefy poner los datos en cascada allí.

Con esto en su lugar, cuando elimina una fila principal, SqlAlchemy ejecutará declaraciones de eliminación para que pueda limpiar las filas secundarias. Es probable que esto no sea tan eficiente como dejar que esta base de datos se encargue de usted, así que no lo recomiendo.

Aquí están los documentos de SqlAlchemy sobre las funciones en cascada que admite.

d512
fuente
Gracias por la explicación. Ahora tiene sentido.
Odin
1
¿Por qué tampoco funciona declarar a Columnen la tabla secundaria como ForeignKey('parent.id', ondelete='cascade', onupdate='cascade')no funciona? Esperaba que los niños fueran eliminados cuando su fila de la tabla principal también se eliminara. En su lugar, SQLA establece los elementos secundarios en a parent.id=NULLo los deja "como están", pero no los elimina. Eso es después de definir originalmente relationshipen el padre como children = relationship('Parent', backref='parent')o relationship('Parent', backref=backref('parent', passive_deletes=True)); DB muestra cascadereglas en el DDL (prueba de concepto basada en SQLite3). Pensamientos
code_dredd
1
Además, debo tener en cuenta que cuando lo uso backref=backref('parent', passive_deletes=True)recibo la siguiente advertencia:, SAWarning: On Parent.children, 'passive_deletes' is normally configured on one-to-many, one-to-one, many-to-many relationships only. "relationships only." % selflo que sugiere que no le gusta el uso de passive_deletes=Trueen esta (obvia) relación entre padres e hijos por alguna razón.
code_dredd
Gran explicación Una pregunta: ¿es deleteredundante cascade='all,delete'?
zaggi
1
@zaggi deleteES redundante cascade='all,delete', ya que de acuerdo con los documentos de SQLAlchemy , alles sinónimo de:save-update, merge, refresh-expire, expunge, delete
pmsoltani
7

Steven tiene razón en que necesita crear explícitamente el backref, esto da como resultado que la cascada se aplique al padre (en lugar de que se aplique al hijo como en el escenario de prueba).

Sin embargo, definir la relación en Child NO hace que sqlalchemy considere Child como padre. No importa dónde se defina la relación (hijo o padre), es la clave externa que vincula las dos tablas que determina cuál es el padre y cuál es el hijo.

Sin embargo, tiene sentido ceñirse a una convención y, según la respuesta de Steven, estoy definiendo todas las relaciones de mis hijos en el padre.

Larry Weya
fuente
6

También tuve problemas con la documentación, pero descubrí que las cadenas de documentos en sí tienden a ser más fáciles que el manual. Por ejemplo, si importa la relación de sqlalchemy.orm y ayuda (relación), le dará todas las opciones que puede especificar para cascada. La bala de delete-orphandice:

Si se detecta un elemento del tipo del niño sin padre, márquelo para eliminarlo.
Tenga en cuenta que esta opción evita que un elemento pendiente de la clase del niño se conserve sin la presencia de un padre.

Me doy cuenta de que su problema fue más con la forma en que la documentación para definir las relaciones entre padres e hijos. Pero parecía que también podría tener un problema con las opciones en cascada, porque "all"incluye "delete". "delete-orphan"es la única opción que no está incluida en "all".

Profano
fuente
¡Usar help(..)en los sqlalchemyobjetos ayuda mucho! Gracias :-))) ! PyCharm no muestra nada en los muelles de contexto, y simplemente se olvidó de verificar el help. ¡Muchas gracias!
dmitry_romanov
5

La respuesta de Steven es sólida. Me gustaría señalar una implicación adicional.

Mediante el uso relationship , está haciendo que la capa de la aplicación (Flask) sea responsable de la integridad referencial. Eso significa que otros procesos que acceden a la base de datos no a través de Flask, como una utilidad de base de datos o una persona que se conecta directamente a la base de datos, no experimentarán esas restricciones y podrían cambiar sus datos de una manera que rompa el modelo lógico de datos que trabajó tan duro para diseñar. .

Siempre que sea posible, utilice el ForeignKeyenfoque descrito por d512 y Alex. El motor de base de datos es muy bueno para realmente hacer cumplir las restricciones (de una manera inevitable), por lo que esta es, con mucho, la mejor estrategia para mantener la integridad de los datos. El único momento en que necesita confiar en una aplicación para manejar la integridad de los datos es cuando la base de datos no puede manejarlos, por ejemplo, versiones de SQLite que no admiten claves externas.

Si necesita crear más vínculos entre entidades para habilitar comportamientos de la aplicación como navegar por las relaciones de objeto principal-secundario, use backrefjunto con ForeignKey.

Chris Johnson
fuente
2

La respuesta de Stevan es perfecta. Pero si sigue recibiendo el error. Otro posible intento además de eso sería:

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

Copiado del enlace

Consejo rápido si tiene problemas con una dependencia de clave externa, incluso si ha especificado una eliminación en cascada en sus modelos.

Usando SQLAlchemy, para especificar una eliminación en cascada que debe tener cascade='all, delete'en su tabla principal. Ok, pero luego cuando ejecutas algo como:

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

En realidad, desencadena un error sobre una clave externa utilizada en las tablas de sus hijos.

La solución la usé para consultar el objeto y luego eliminarlo:

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)

Esto debería eliminar su registro principal Y todos los hijos asociados con él.

Prashant Momale
fuente
1
¿Es .first()necesario llamar ? ¿Qué condiciones de filtro devuelven una lista de objetos y todo debe eliminarse? ¿La llamada no .first()obtiene solo el primer objeto? @Prashant
Kavin Raju S
2

La respuesta de Alex Okrushko casi funcionó mejor para mí. Usado ondelete = 'CASCADE' y passive_deletes = True combinados. Pero tuve que hacer algo extra para que funcione para 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)

Asegúrese de agregar este código para asegurarse de que funcione para 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()

Robado de aquí: lenguaje de expresión SQLAlchemy y SQLite en cascada de eliminación

estupido estudiante
fuente
0

TLDR: si las soluciones anteriores no funcionan, intente agregar nullable = False a su columna.

Me gustaría agregar un pequeño punto aquí para algunas personas que pueden no lograr que la función en cascada funcione con las soluciones existentes (que son excelentes). La principal diferencia entre mi trabajo y el ejemplo fue que usé automap. No sé exactamente cómo eso podría interferir con la configuración de las cascadas, pero quiero señalar que lo usé. También estoy trabajando con una base de datos SQLite.

Probé todas las soluciones descritas aquí, pero las filas de mi tabla secundaria continuaron teniendo su clave externa establecida en nula cuando se eliminó la fila principal. Probé todas las soluciones aquí en vano. Sin embargo, la cascada funcionó una vez que configuré la columna secundaria con la clave externa en nullable = False.

En la mesa secundaria, agregué:

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

Con esta configuración, la cascada funcionó como se esperaba.

Spencer Weston
fuente