SQLAlchemy ORM - Eliminación de objetos relacionados

Es fácil realizar la operación de eliminación en una sola tabla. Todo lo que tiene que hacer es eliminar un objeto de la clase asignada de una sesión y confirmar la acción. Sin embargo, la operación de eliminación en varias tablas relacionadas es un poco complicada.

En nuestra base de datos sales.db, las clases de clientes y facturas se asignan a la tabla de clientes y facturas con uno a varios tipos de relación. Intentaremos eliminar el objeto Cliente y veremos el resultado.

Como referencia rápida, a continuación se encuentran las definiciones de las clases de cliente y factura:

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 una sesión y obtenemos un objeto Cliente al consultarlo con la identificación principal utilizando el programa a continuación:

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

En nuestra tabla de muestra, x.name resulta ser 'Gopal Krishna'. Eliminemos esta x de la sesión y contemos la aparición de este nombre.

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

La expresión SQL resultante devolverá 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

Sin embargo, los objetos Factura relacionados de x todavía están allí. Se puede verificar con el siguiente código:

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

Aquí, 10 y 14 son números de factura que pertenecen al cliente Gopal Krishna. El resultado de la consulta anterior es 2, lo que significa que los objetos relacionados no se han eliminado.

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

Esto se debe a que SQLAlchemy no asume la eliminación de cascada; tenemos que dar un comando para borrarlo.

Para cambiar el comportamiento, configuramos opciones en cascada en la relación User.addresses. Cerremos la sesión en curso, usemos new declarative_base () y redeclaremos la clase User, agregando la relación de direcciones, incluida la configuración en cascada.

El atributo de cascada en la función de relación es una lista separada por comas de reglas en cascada que determina cómo las operaciones de la sesión deben ser "en cascada" de padre a hijo. De forma predeterminada, es Falso, lo que significa que es "guardar-actualizar, fusionar".

Las cascadas disponibles son las siguientes:

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

La opción que se usa con frecuencia es "todo, eliminar-huérfano" para indicar que los objetos relacionados deben seguir al objeto principal en todos los casos y eliminarse cuando se desasocie.

Por lo tanto, la clase de cliente redeclarada se muestra a continuación:

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

Eliminemos el Cliente con el nombre de Gopal Krishna utilizando el programa a continuación y veamos el recuento de sus objetos de Factura 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()

El recuento ahora es 0 con el siguiente SQL emitido por el script anterior:

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