SQLAlchemy: exclusão em cascata

116

Devo estar faltando algo trivial com as opções de cascata do SQLAlchemy porque não consigo fazer uma exclusão em cascata simples operar corretamente - se um elemento pai for excluído, os filhos persistem, com nullchaves estrangeiras.

Coloquei um caso de teste conciso aqui:

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

Resultado:

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

Existe uma relação simples de um para muitos entre pai e filho. O script cria um pai, adiciona 3 filhos e depois confirma. Em seguida, ele exclui o pai, mas os filhos persistem. Por quê? Como faço para excluir os filhos em cascata?

Carl
fonte
Esta seção nos documentos (pelo menos agora, 3 anos depois da postagem original) parece bastante útil nisso: docs.sqlalchemy.org/en/rel_0_9/orm/session.html#cascades
Soferio

Respostas:

184

O problema é que sqlalchemy considera Childcomo o pai, porque é onde você definiu seu relacionamento (não importa se você o chamou de "Filho" é claro).

Se você definir o relacionamento na Parentclasse, funcionará:

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

(observe "Child"como uma string: isso é permitido ao usar o estilo declarativo, para que você possa se referir a uma classe que ainda não foi definida)

Você pode querer adicionar delete-orphantambém ( deletefaz com que os filhos sejam excluídos quando o pai é excluído, delete-orphantambém exclui todos os filhos que foram "removidos" do pai, mesmo se o pai não for excluído)

EDIT: acabei de descobrir: se você realmente deseja definir o relacionamento na Childclasse, pode fazê-lo, mas terá que definir a cascata no backref (criando o backref explicitamente), assim:

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

(implicando from sqlalchemy.orm import backref)

Steven
fonte
6
Aha, é isso. Gostaria que a documentação fosse mais explícita sobre isso!
carl
15
Sim. Muito útil. Sempre tive problemas com a documentação do SQLAlchemy.
ayaz de
1
Isso está bem explicado no documento docs.sqlalchemy.org/en/rel_0_9/orm/cascades.html
Epoc
1
@Lyman Zerga: no exemplo do OP: se você remover um Childobjeto de parent.children, esse objeto deve ser excluído do banco de dados ou apenas sua referência ao pai deve ser removida (ou seja, definir parentidcoluna como nula, em vez de excluir a linha)
Steven
1
Espere, relationshipnão dita a configuração pai-filho. Usar ForeignKeysobre a mesa é o que o configura como criança. Não importa se relationshipé do pai ou da criança.
d512
110

A resposta de @Steven é boa quando você está deletando através do session.delete()que nunca acontece no meu caso. Percebi que na maioria das vezes eu apago através session.query().filter().delete()(o que não coloca elementos na memória e apaga diretamente do db). Usar este método sqlalchemy cascade='all, delete'não funciona. Porém, há uma solução: ON DELETE CASCADEpor meio de db (nota: nem todos os bancos de dados o suportam).

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
fonte
3
Obrigado por explicar essa diferença - Eu estava tentando usar session.query().filter().delete()e lutando para encontrar o problema
nighthawk454
4
Eu tive que definir passive_deletes='all'para que os filhos fossem excluídos pela cascata do banco de dados quando o pai fosse excluído. Com passive_deletes=True, os objetos filhos estavam sendo desassociados (pai definido como NULL) antes de o pai ser excluído, portanto, a cascata do banco de dados não estava fazendo nada.
Milorad Pop-Tosic
@ MiloradPop-Tosic Não uso o SQLAlchemy há mais de 3 anos, mas ler o documento parece passive_deletes = True ainda é a coisa certa.
Alex Okrushko,
2
Posso confirmar que passive_deletes=Truefunciona corretamente neste cenário.
d512
Eu estava tendo problemas com as revisões de geração automática do alambique que incluíam cascata ao excluir - essa era a resposta.
JNW de
105

Postagem bem antiga, mas acabei de gastar uma ou duas horas nisso, então gostaria de compartilhar minha descoberta, especialmente porque alguns dos outros comentários listados não estão certos.

TL; DR

Dê à tabela filho um estrangeiro ou modifique o existente, adicionando ondelete='CASCADE':

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

E um dos seguintes relacionamentos:

a) Isso na tabela pai:

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

b) Ou isso na mesa infantil:

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

Detalhes

Em primeiro lugar, apesar do que diz a resposta aceita, a relação pai / filho não se estabelece usando relationship, é estabelecida usando ForeignKey. Você pode colocar o relationshipnas tabelas pai ou filho e funcionará bem. Embora, aparentemente nas tabelas filho, você tenha que usar a backreffunção além do argumento de palavra-chave.

Opção 1 (preferencial)

Em segundo lugar, SqlAlchemy oferece suporte a dois tipos diferentes de cascata. O primeiro, e o que eu recomendo, está embutido em seu banco de dados e geralmente assume a forma de uma restrição na declaração de chave estrangeira. No PostgreSQL, é assim:

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

Isso significa que quando você exclui um registro de parent_table, todas as linhas correspondentes em child_tableserão excluídas para você pelo banco de dados. É rápido e confiável e provavelmente sua melhor aposta. Você configura isso no SqlAlchemy ForeignKeyassim (parte da definição da tabela filho):

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

A ondelete='CASCADE'é a parte que cria o ON DELETE CASCADEsobre a mesa.

Peguei vocês!

Há uma advertência importante aqui. Observe como eu tenho um relationshipespecificado com passive_deletes=True? Se você não tiver isso, a coisa toda não funcionará. Isso ocorre porque, por padrão, quando você exclui um registro pai, o SqlAlchemy faz algo realmente estranho. Ele define as chaves estrangeiras de todas as linhas filhas como NULL. Portanto, se você excluir uma linha de parent_tableonde id= 5, ela basicamente executará

UPDATE child_table SET parent_id = NULL WHERE parent_id = 5

Por que você iria querer isso, eu não tenho ideia. Eu ficaria surpreso se muitos mecanismos de banco de dados permitissem que você definisse uma chave estrangeira válida para NULL, criando um órfão. Parece uma má ideia, mas talvez haja um caso de uso. De qualquer forma, se você deixar o SqlAlchemy fazer isso, você impedirá que o banco de dados seja capaz de limpar os filhos usando o ON DELETE CASCADEque você configurou. Isso ocorre porque ele depende dessas chaves estrangeiras para saber quais linhas filho excluir. Depois que o SqlAlchemy definir todos como NULL, o banco de dados não poderá excluí-los. A configuração passive_deletes=Trueevita que o SqlAlchemy NULLsaia das chaves estrangeiras.

Você pode ler mais sobre exclusões passivas nos documentos do SqlAlchemy .

opção 2

A outra maneira de fazer isso é deixar o SqlAlchemy fazer isso por você. Isso é configurado usando o cascadeargumento do relationship. Se você tiver o relacionamento definido na tabela pai, será assim:

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

Se o relacionamento é por conta da criança, você faz assim:

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

Novamente, este é o filho, então você deve chamar um método chamado backrefe colocar os dados em cascata nele.

Com isso no lugar, quando você exclui uma linha pai, SqlAlchemy irá realmente executar instruções delete para você limpar as linhas filho. Provavelmente, isso não será tão eficiente quanto deixar esse banco de dados manipular se for para você, então eu não o recomendo.

Aqui estão os documentos do SqlAlchemy sobre os recursos em cascata suportados.

d512
fonte
Obrigado pela explicação. Agora faz sentido.
Odin
1
Por que declarar a Columnna tabela filho como ForeignKey('parent.id', ondelete='cascade', onupdate='cascade')não funciona também? Eu esperava que os filhos fossem excluídos quando a linha da tabela pai também fosse excluída. Em vez disso, o SQLA define os filhos como a parent.id=NULLou os deixa "como estão", mas não exclui. Isso depois de definir originalmente o relationshipno pai como children = relationship('Parent', backref='parent')ou relationship('Parent', backref=backref('parent', passive_deletes=True)); O DB mostra cascaderegras no DDL (prova de conceito baseada em SQLite3). Pensamentos?
code_dredd
1
Além disso, devo observar que, quando uso backref=backref('parent', passive_deletes=True), recebo o seguinte aviso:, SAWarning: On Parent.children, 'passive_deletes' is normally configured on one-to-many, one-to-one, many-to-many relationships only. "relationships only." % selfsugerindo que não gosto do uso de passive_deletes=Truenesta (óbvia) relação pai-filho (óbvia) um para muitos por algum motivo.
code_dredd
Ótima explicação. Uma pergunta - é deleteredundante em cascade='all,delete'?
zaggi
1
@zaggi deleteÉ redundante em cascade='all,delete', pois de acordo com os documentos do SQLAlchemy , allé sinônimo de:save-update, merge, refresh-expire, expunge, delete
pmsoltani
7

Steven está correto ao dizer que você precisa criar explicitamente o backref, o que resulta na aplicação da cascata no pai (ao contrário de ser aplicada no filho como no cenário de teste).

No entanto, definir o relacionamento no filho NÃO faz a sqlalchemy considerar o filho o pai. Não importa onde o relacionamento está definido (filho ou pai), é a chave estrangeira que liga as duas tabelas que determina qual é o pai e qual é o filho.

No entanto, faz sentido seguir uma convenção e, com base na resposta de Steven, estou definindo todos os meus relacionamentos de filho no pai.

Larry Weya
fonte
6

Eu também tive problemas com a documentação, mas descobri que as próprias docstrings tendem a ser mais fáceis do que o manual. Por exemplo, se você importar relacionamento de sqlalchemy.orm e ajudar (relacionamento), ele fornecerá todas as opções que você pode especificar para cascata. O marcador para delete-orphandiz:

se um item do tipo da criança sem pai for detectado, marque-o para exclusão.
Observe que esta opção evita que um item pendente da classe do filho seja persistido sem a presença de um pai.

Sei que seu problema era mais com a forma como a documentação para definir as relações pai-filho. Mas parecia que você também pode estar tendo problemas com as opções em cascata, porque "all"inclui "delete". "delete-orphan"é a única opção que não está incluída em "all".

Profano
fonte
Usar help(..)nos sqlalchemyobjetos ajuda muito! Obrigado :-))) ! O PyCharm não mostra nada nas docas de contexto e simplesmente se esqueceu de verificar o help. Muito obrigado!
dmitry_romanov
5

A resposta de Steven é sólida. Eu gostaria de apontar uma implicação adicional.

Ao usar relationship, você está tornando a camada de aplicativo (Flask) responsável pela integridade referencial. Isso significa que outros processos que acessam o banco de dados não por meio do Flask, como um utilitário de banco de dados ou uma pessoa se conectando ao banco de dados diretamente, não terão essas restrições e podem alterar seus dados de uma forma que quebra o modelo de dados lógico que você trabalhou tão arduamente para projetar .

Sempre que possível, use a ForeignKeyabordagem descrita por d512 e Alex. O mecanismo de banco de dados é muito bom em realmente impor restrições (de maneira inevitável), portanto, essa é de longe a melhor estratégia para manter a integridade dos dados. O único momento em que você precisa confiar em um aplicativo para lidar com a integridade dos dados é quando o banco de dados não pode lidar com eles, por exemplo, versões do SQLite que não oferecem suporte a chaves estrangeiras.

Se você precisar criar vínculos adicionais entre entidades para habilitar comportamentos de aplicativos, como navegar em relacionamentos de objeto pai-filho, use backrefem conjunto com ForeignKey.

Chris Johnson
fonte
2

A resposta de Stevan é perfeita. Mas se você ainda está recebendo o erro. Outra possível tentativa além disso seria -

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

Copiado do link-

Dica rápida se você tiver problemas com uma dependência de chave estrangeira, mesmo que tenha especificado uma exclusão em cascata em seus modelos.

Usando SQLAlchemy, para especificar uma exclusão em cascata que você deve ter cascade='all, delete'em sua tabela pai. Ok, mas quando você executa algo como:

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

Na verdade, ele dispara um erro sobre uma chave estrangeira usada nas tabelas filhas.

A solução que usei para consultar o objeto e excluí-lo:

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)

Isso deve excluir seu registro pai E todos os filhos associados a ele.

Prashant Momale
fonte
1
É .first()necessário ligar ? Quais condições de filtro retornam uma lista de objetos e tudo deve ser excluído? A chamada não .first()obtém apenas o primeiro objeto? @Prashant
Kavin Raju S
2

A resposta de Alex Okrushko quase funcionou melhor para mim. Ondelete = 'CASCADE' e passive_deletes = True combinados usados. Mas eu tive que fazer algo extra para fazê-lo funcionar no 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)

Certifique-se de adicionar este código para garantir que ele 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()

Roubado daqui: linguagem de expressão SQLAlchemy e SQLite em cascata de exclusão

estudante estúpido
fonte
0

TLDR: se as soluções acima não funcionarem, tente adicionar nullable = False à sua coluna.

Eu gostaria de acrescentar um pequeno ponto aqui para algumas pessoas que podem não conseguir que a função cascata funcione com as soluções existentes (que são ótimas). A principal diferença entre o meu trabalho e o exemplo é que usei o automap. Não sei exatamente como isso pode interferir na configuração das cascatas, mas quero ressaltar que usei. Também estou trabalhando com um banco de dados SQLite.

Tentei todas as soluções descritas aqui, mas as linhas em minha tabela filho continuaram a ter sua chave estrangeira definida como nula quando a linha pai foi excluída. Eu tentei todas as soluções aqui sem sucesso. No entanto, a cascata funcionou assim que defini a coluna filho com a chave estrangeira como nullable = False.

Na mesa filho, adicionei:

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

Com essa configuração, a cascata funcionou conforme o esperado.

Spencer Weston
fonte