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 null
claves 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?
python
database
sqlalchemy
carl
fuente
fuente
Respuestas:
El problema es que sqlalchemy considera
Child
como 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
Parent
clase en su lugar, funcionará:(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
(delete
hace que los niños se eliminen cuando se elimina el padre,delete-orphan
tambié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
Child
clase, puede hacerlo, pero tendrá que definir la cascada en el backref (creando el backref explícitamente), así:(implicando
from sqlalchemy.orm import backref
)fuente
Child
objeto a partir deparent.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.parentid
Columna a null, en lugar de eliminar la fila)relationship
no dicta la configuración padre-hijo. UsarloForeignKey
en una mesa es lo que lo configura como niño. No importa sirelationship
es del padre o del hijo.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éssession.query().filter().delete()
(lo que no coloca elementos en la memoria y elimina directamente de la base de datos). Usar este método sqlalchemy'scascade='all, delete'
no funciona. Sin embargo, hay una solución: aON DELETE CASCADE
través de db (nota: no todas las bases de datos lo admiten).fuente
session.query().filter().delete()
y luchando por encontrar el problemapassive_deletes='all'
para que los niños fueran eliminados por la cascada de la base de datos cuando se elimina el padre. Conpassive_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.passive_deletes=True
funciona correctamente en este escenario.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'
:Y una de las siguientes relaciones:
a) Esto en la mesa principal:
b) O esto en la mesa del niño:
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 usoForeignKey
. Puede poner elrelationship
en la tabla principal o secundaria y funcionará bien. Aunque, aparentemente en las tablas secundarias, debe usar labackref
funció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í:
Esto significa que cuando elimine un registro de
parent_table
,child_table
la 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):los
ondelete='CASCADE'
es la parte que crea elON DELETE CASCADE
sobre la mesa.¡Te tengo!
Aquí hay una advertencia importante. ¿Observa cómo he
relationship
especificado conpassive_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 enNULL
. Entonces, si elimina una fila deparent_table
dondeid
= 5, básicamente se ejecutará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
NULL
crear 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 archivoON DELETE CASCADE
que 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 todosNULL
, la base de datos no puede eliminarlos. Establecer elpassive_deletes=True
evita que SqlAlchemy extraigaNULL
las 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
cascade
argumento derelationship
. Si tiene la relación definida en la tabla principal, se ve así:Si la relación es del niño, hazlo así:
Nuevamente, este es el hijo, por lo que debe llamar a un método llamado
backref
y 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.
fuente
Column
en la tabla secundaria comoForeignKey('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 aparent.id=NULL
o los deja "como están", pero no los elimina. Eso es después de definir originalmenterelationship
en el padre comochildren = relationship('Parent', backref='parent')
orelationship('Parent', backref=backref('parent', passive_deletes=True))
; DB muestracascade
reglas en el DDL (prueba de concepto basada en SQLite3). Pensamientosbackref=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." % self
lo que sugiere que no le gusta el uso depassive_deletes=True
en esta (obvia) relación entre padres e hijos por alguna razón.delete
redundantecascade='all,delete'
?delete
ES redundantecascade='all,delete'
, ya que de acuerdo con los documentos de SQLAlchemy ,all
es sinónimo de:save-update, merge, refresh-expire, expunge, delete
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.
fuente
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-orphan
dice: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"
.fuente
help(..)
en lossqlalchemy
objetos ayuda mucho! Gracias :-))) ! PyCharm no muestra nada en los muelles de contexto, y simplemente se olvidó de verificar elhelp
. ¡Muchas gracias!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
ForeignKey
enfoque 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
backref
junto conForeignKey
.fuente
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: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:
Esto debería eliminar su registro principal Y todos los hijos asociados con él.
fuente
.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? @PrashantLa 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.
Asegúrese de agregar este código para asegurarse de que funcione para sqlite.
Robado de aquí: lenguaje de expresión SQLAlchemy y SQLite en cascada de eliminación
fuente
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é:
Con esta configuración, la cascada funcionó como se esperaba.
fuente