SQLAlchemy ORM - Excluindo Objetos Relacionados

É fácil realizar a operação de exclusão em uma única tabela. Tudo que você precisa fazer é excluir um objeto da classe mapeada de uma sessão e confirmar a ação. No entanto, a operação de exclusão em várias tabelas relacionadas é um pouco complicada.

Em nosso banco de dados sales.db, as classes Cliente e Fatura são mapeadas para o cliente e a tabela de fatura com um para vários tipos de relacionamento. Tentaremos excluir o objeto Cliente e ver o resultado.

Como uma referência rápida, abaixo estão as definições das classes Cliente e Fatura -

from sqlalchemy import create_engine, ForeignKey, Column, Integer, String
engine = create_engine('sqlite:///sales.db', echo = True)
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
from sqlalchemy.orm import relationship
class Customer(Base):
   __tablename__ = 'customers'

   id = Column(Integer, primary_key = True)
   name = Column(String)
   address = Column(String)
   email = Column(String)
   
class Invoice(Base):
   __tablename__ = 'invoices'

   id = Column(Integer, primary_key = True)
   custid = Column(Integer, ForeignKey('customers.id'))
   invno = Column(Integer)
   amount = Column(Integer)
   customer = relationship("Customer", back_populates = "invoices")
   
Customer.invoices = relationship("Invoice", order_by = Invoice.id, back_populates = "customer")

Configuramos uma sessão e obtemos um objeto Cliente, consultando-o com o ID primário usando o programa abaixo -

from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=engine)
session = Session()
x = session.query(Customer).get(2)

Em nossa tabela de exemplo, x.name é 'Gopal Krishna'. Vamos deletar esse x da sessão e contar a ocorrência desse nome.

session.delete(x)
session.query(Customer).filter_by(name = 'Gopal Krishna').count()

A expressão SQL resultante retornará 0.

SELECT count(*) 
AS count_1
FROM (
   SELECT customers.id 
   AS customers_id, customers.name 
   AS customers_name, customers.address 
   AS customers_address, customers.email 
   AS customers_email
   FROM customers
   WHERE customers.name = ?) 
AS anon_1('Gopal Krishna',) 0

No entanto, os objetos de fatura relacionados de x ainda estão lá. Pode ser verificado pelo seguinte código -

session.query(Invoice).filter(Invoice.invno.in_([10,14])).count()

Aqui, 10 e 14 são os números das faturas pertencentes ao cliente Gopal Krishna. O resultado da consulta acima é 2, o que significa que os objetos relacionados não foram excluídos.

SELECT count(*) 
AS count_1
FROM (
   SELECT invoices.id 
   AS invoices_id, invoices.custid 
   AS invoices_custid, invoices.invno 
   AS invoices_invno, invoices.amount 
   AS invoices_amount
   FROM invoices
   WHERE invoices.invno IN (?, ?)) 
AS anon_1(10, 14) 2

Isso ocorre porque SQLAlchemy não assume a exclusão da cascata; temos que dar um comando para excluí-lo.

Para alterar o comportamento, configuramos opções em cascata no relacionamento User.addresses. Vamos fechar a sessão em andamento, usar new declarative_base () e declarar novamente a classe User, adicionando o relacionamento de endereços, incluindo a configuração em cascata.

O atributo cascade na função de relacionamento é uma lista separada por vírgulas de regras em cascata que determina como as operações da Sessão devem ser “cascateadas” de pai para filho. Por padrão, é False, o que significa que é "salvar-atualizar, mesclar".

As cascatas disponíveis são as seguintes -

  • save-update
  • merge
  • expunge
  • delete
  • delete-orphan
  • refresh-expire

A opção frequentemente usada é "todos, excluir órfãos" para indicar que os objetos relacionados devem seguir junto com o objeto pai em todos os casos e ser excluídos quando desassociados.

Portanto, a classe Cliente reafirmada é mostrada abaixo -

class Customer(Base): 
   __tablename__ = 'customers'
   
   id = Column(Integer, primary_key = True) 
   name = Column(String) 
   address = Column(String) 
   email = Column(String) 
   invoices = relationship(
      "Invoice", 
      order_by = Invoice.id, 
      back_populates = "customer",
      cascade = "all, 
      delete, delete-orphan" 
   )

Vamos excluir o Cliente com o nome Gopal Krishna usando o programa abaixo e ver a contagem de seus objetos de fatura relacionados -

from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind = engine)
session = Session()
x = session.query(Customer).get(2)
session.delete(x)
session.query(Customer).filter_by(name = 'Gopal Krishna').count()
session.query(Invoice).filter(Invoice.invno.in_([10,14])).count()

A contagem agora é 0 com o seguinte SQL emitido pelo script acima -

SELECT customers.id 
AS customers_id, customers.name 
AS customers_name, customers.address 
AS customers_address, customers.email 
AS customers_email
FROM customers
WHERE customers.id = ?
(2,)
SELECT invoices.id 
AS invoices_id, invoices.custid 
AS invoices_custid, invoices.invno 
AS invoices_invno, invoices.amount
AS invoices_amount
FROM invoices
WHERE ? = invoices.custid 
ORDER BY invoices.id (2,)
DELETE FROM invoices 
WHERE invoices.id = ? ((1,), (2,))
DELETE FROM customers 
WHERE customers.id = ? (2,)
SELECT count(*) 
AS count_1
FROM (
   SELECT customers.id 
   AS customers_id, customers.name 
   AS customers_name, customers.address 
   AS customers_address, customers.email 
   AS customers_email
   FROM customers
   WHERE customers.name = ?) 
AS anon_1('Gopal Krishna',)
SELECT count(*) 
AS count_1
FROM (
   SELECT invoices.id 
   AS invoices_id, invoices.custid 
   AS invoices_custid, invoices.invno 
   AS invoices_invno, invoices.amount 
   AS invoices_amount
   FROM invoices
   WHERE invoices.invno IN (?, ?)) 
AS anon_1(10, 14)
0