Many-to-Many con objeto de asociación y todas las relaciones definidas se bloquea al eliminar

Cuando se tiene un conjunto de muchos a muchos con todas las relaciones descritas, se elimina la eliminación de uno de los dos objetos principales.

Descripción

Car ( .car_ownerships ) ( .car ) CarOwnership ( .person ) ( .car_ownerships ) Persona

Car ( .people ) ( .cars ) Persona

Problema

Cuando se elimina un Car o una Person SA, primero se elimina el objeto de asociación CarOwnership (debido a la relación ‘ directa ‘ con el argumento secondary ) y luego se intenta actualizar las claves externas a NULL en los mismos objetos de asociación, por lo tanto, se bloquea.

¿Cómo debo resolver esto? Estoy un poco perplejo al ver que esto no se aborda en los documentos ni en ningún lugar que pueda encontrar en línea, ya que pensé que este patrón era bastante común: – /. ¿Qué me estoy perdiendo?

Sé que podría tener el interruptor passive_deletes para la relación a través, pero me gustaría mantener la statement de eliminación, solo para evitar que ocurra la actualización o (haga que suceda antes).

Edición : en realidad, passive_deletes no resuelve el problema si los objetos dependientes se cargan en la sesión, ya que aún se emitirá la instrucción DELETE . Una solución es usar viewonly=True , pero luego pierdo no solo la eliminación sino la creación automática de objetos de asociación. También me parece que viewonly=True es bastante peligroso, porque te permite append() sin persistir.

REPEX

Preparar

 from sqlalchemy import create_engine, Table, Column, Integer, String, ForeignKey from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship, backref, sessionmaker engine = create_engine('sqlite:///:memory:', echo = False) Base = declarative_base() Session = sessionmaker(bind=engine) session = Session() class Person(Base): __tablename__ = 'persons' id = Column(Integer(), primary_key=True) name = Column(String(255)) cars = relationship('Car', secondary='car_ownerships', backref='people') def __repr__(self): return ''.format(self.name, self.id) class Car(Base): __tablename__ = 'cars' id = Column(Integer(), primary_key=True) name = Column(String(255)) def __repr__(self): return ''.format(self.name, self.id) class CarOwnership(Base): __tablename__ = 'car_ownerships' id = Column(Integer(), primary_key=True) type = Column(String(255)) car_id = Column(Integer(), ForeignKey(Car.id)) car = relationship('Car', backref='car_ownerships') person_id = Column(Integer(), ForeignKey(Person.id)) person = relationship('Person', backref='car_ownerships') def __repr__(self): return 'Ownership [{}]: {} <> {}'.format(self.id, self.car, self.type, self.person) Base.metadata.create_all(engine) 

Objetos de archivo

 antoine = Person(name='Antoine') rob = Person(name='Rob') car1 = Car(name="Honda Civic") car2 = Car(name='Renault Espace') CarOwnership(person=antoine, car=car1, type = "secondary") CarOwnership(person=antoine, car=car2, type = "primary") CarOwnership(person=rob, car=car1, type = "primary") session.add(antoine) session.commit() session.query(CarOwnership).all() 

Eliminando -> Accidente

 print('#### DELETING') session.delete(car1) print('#### COMMITING') session.commit() # StaleDataError Traceback (most recent call last) #  in () # 1 session.delete(car1) # ----> 2 session.commit() # ... 

Diagnósticos

La explicación que propongo anteriormente está respaldada por las sentencias de SQL dadas por el motor con echo=True :

 #### DELETING #### COMMITING 2016-07-07 16:55:28,893 INFO sqlalchemy.engine.base.Engine SELECT persons.id AS persons_id, persons.name AS persons_name FROM persons, car_ownerships WHERE ? = car_ownerships.car_id AND persons.id = car_ownerships.person_id 2016-07-07 16:55:28,894 INFO sqlalchemy.engine.base.Engine (1,) 2016-07-07 16:55:28,895 INFO sqlalchemy.engine.base.Engine SELECT car_ownerships.id AS car_ownerships_id, car_ownerships.type AS car_ownerships_type, car_ownerships.car_id AS car_ownerships_car_id, car_ownerships.person_id AS car_ownerships_person_id FROM car_ownerships WHERE ? = car_ownerships.car_id 2016-07-07 16:55:28,896 INFO sqlalchemy.engine.base.Engine (1,) 2016-07-07 16:55:28,898 INFO sqlalchemy.engine.base.Engine DELETE FROM car_ownerships WHERE car_ownerships.car_id = ? AND car_ownerships.person_id = ? 2016-07-07 16:55:28,898 INFO sqlalchemy.engine.base.Engine ((1, 1), (1, 2)) 2016-07-07 16:55:28,900 INFO sqlalchemy.engine.base.Engine UPDATE car_ownerships SET car_id=? WHERE car_ownerships.id = ? 2016-07-07 16:55:28,900 INFO sqlalchemy.engine.base.Engine ((None, 1), (None, 2)) 2016-07-07 16:55:28,901 INFO sqlalchemy.engine.base.Engine ROLLBACK 

EDICIONES

Usando association_proxy

Podemos usar proxies de asociación para intentar materializar la relación ‘a través’.

Sin embargo, con el fin de .append() un objeto dependiente directamente, necesitamos crear un constructor para el objeto de asociación. Este constructor debe ser “pirateado” para que sea bidireccional, de modo que podamos usar ambas tareas:

 my_car.people.append(Person(name='my_son')) my_husband.cars.append(Car(name='new_shiny_car')) 

El código resultante (a mitad de la prueba) está abajo, pero no me siento muy cómodo con él (¿qué más se va a romper a causa de este hacky constructor?).

EDITAR: El camino a seguir con los servidores proxy de asociación se presenta en la respuesta de RazerM a continuación. association_proxy() tiene un argumento de creador que alivia la necesidad del constructor monstruoso que terminé usando a continuación.

 class Person(Base): __tablename__ = 'persons' id = Column(Integer(), primary_key=True) name = Column(String(255)) cars = association_proxy('car_ownerships', 'car') def __repr__(self): return ''.format(self.name, self.id) class Car(Base): __tablename__ = 'cars' id = Column(Integer(), primary_key=True) name = Column(String(255)) people = association_proxy('car_ownerships', 'person') def __repr__(self): return ''.format(self.name, self.id) class CarOwnership(Base): __tablename__ = 'car_ownerships' id = Column(Integer(), primary_key=True) type = Column(String(255)) car_id = Column(Integer(), ForeignKey(Car.id)) car = relationship('Car', backref='car_ownerships') person_id = Column(Integer(), ForeignKey(Person.id)) person = relationship('Person', backref='car_ownerships') def __init__(self, car=None, person=None, type='secondary'): if isinstance(car, Person): car, person = person, car self.car = car self.person = person self.type = type def __repr__(self): return 'Ownership [{}]: {} <> {}'.format(self.id, self.car, self.type, self.person) 

Estás utilizando un objeto de asociación , por lo que necesitas hacer las cosas de manera diferente.

He cambiado las relaciones aquí, míralas con cuidado porque al principio es un poco difícil envolverte la cabeza (¡al menos fue para mí!).

He usado back_populates porque es más claro que backref en este caso. Ambos lados de la relación de muchos a muchos deben referirse directamente a CarOwnership , ya que es ese objeto con el que trabajará. Esto es también lo que muestra su ejemplo; necesitas usarlo para que puedas configurar el type .

 class Person(Base): __tablename__ = 'persons' id = Column(Integer(), primary_key=True) name = Column(String(255)) cars = relationship('CarOwnership', back_populates='person') def __repr__(self): return ''.format(self.name, self.id) class Car(Base): __tablename__ = 'cars' id = Column(Integer(), primary_key=True) name = Column(String(255)) people = relationship('CarOwnership', back_populates='car') def __repr__(self): return ''.format(self.name, self.id) class CarOwnership(Base): __tablename__ = 'car_ownerships' id = Column(Integer(), primary_key=True) type = Column(String(255)) car_id = Column(Integer(), ForeignKey(Car.id)) person_id = Column(Integer(), ForeignKey(Person.id)) car = relationship('Car', back_populates='people') person = relationship('Person', back_populates='cars') def __repr__(self): return 'Ownership [{}]: {} <<-{}->> {}'.format(self.id, self.car, self.type, self.person) 

Tenga en cuenta que después de eliminar cualquiera de los lados, la fila car_ownerships no se eliminará, simplemente establecerá las claves foráneas en NULL. Puedo agregar más a mi respuesta si desea configurar la eliminación automática.

Editar: para acceder directamente a las colecciones de objetos Car y Person , debe usar association_proxy , las clases luego cambian a esto:

 from sqlalchemy.ext.associationproxy import association_proxy class Person(Base): __tablename__ = 'persons' id = Column(Integer(), primary_key=True) name = Column(String(255)) cars = association_proxy( 'cars_association', 'car', creator=lambda c: CarOwnership(car=c)) def __repr__(self): return ''.format(self.name, self.id) class Car(Base): __tablename__ = 'cars' id = Column(Integer(), primary_key=True) name = Column(String(255)) people = association_proxy( 'people_association', 'person', creator=lambda p: CarOwnership(person=p)) def __repr__(self): return ''.format(self.name, self.id) class CarOwnership(Base): __tablename__ = 'car_ownerships' id = Column(Integer(), primary_key=True) type = Column(String(255), default='secondary') car_id = Column(Integer(), ForeignKey(Car.id)) person_id = Column(Integer(), ForeignKey(Person.id)) car = relationship('Car', backref='people_association') person = relationship('Person', backref='cars_association') def __repr__(self): return 'Ownership [{}]: {} <<-{}->> {}'.format(self.id, self.car, self.type, self.person) 

Editar: En su edición, cometió un error cuando lo convirtió para usar la backref . Los poderes de su asociación para automóvil y persona no pueden usar la relación ‘car_ownerships’, por lo que tuve uno llamado ‘people_association’ y uno llamado ‘cars_association’.

La relación de ‘car_ownerships’ que tiene no está relacionada con el hecho de que la tabla de asociación se llama ‘car_ownerships’, por lo que los nombré de forma diferente.

He modificado el bloque de código anterior. Para permitir que la aplicación funcione, debe agregar un creador al proxy de asociación. He cambiado back_populates a backref , y backref agregado el type predeterminado al objeto Column lugar del constructor.

La solución más limpia se encuentra a continuación y no implica proxies de asociación. Es la receta faltante para las relaciones de muchos a través de muchos.

Aquí, editamos las relaciones directas que van desde los objetos dependientes Car and Person hasta el objeto de asociación CarOwnership , para evitar que estas relaciones emitan una UPDATE después de que se haya eliminado el objeto de asociación. Para este fin, usamos el passive_deletes='all' .

La interacción resultante es:

  • Capacidad para consultar y establecer el objeto de asociación a partir de los objetos dependientes.
  # Changing Ownership type: my_car.car_ownerships[0].type = 'primary' # Creating an ownership between a car and a person directly: CarOwnership(car=my_car, person=my_husband, type='primary') 
  • Capacidad de acceder y editar objetos dependientes directamente:

     # Get all cars from a person: [print(c) for c in my_husband.cars] # Update the name of one of my cars: me.cars[0].name = me.cars[0].name + ' Cabriolet' 
  • creación y eliminación automáticas del objeto de asociación al crear o eliminar objetos dependientes

     # Create a new owner and assign it to a car: my_car.people.append(Person('my_husband')) session.add(my_car) session.commit() # Creates the necessary CarOwnership # Delete a car: session.delete(my_car) session.commit() # Deletes all the related CarOwnership objects 

Código

 class Person(Base): __tablename__ = 'persons' id = Column(Integer(), primary_key=True) name = Column(String(255)) cars = relationship('Car', secondary='car_ownerships', backref='people') def __repr__(self): return ''.format(self.name, self.id) class Car(Base): __tablename__ = 'cars' id = Column(Integer(), primary_key=True) name = Column(String(255)) def __repr__(self): return ''.format(self.name, self.id) class CarOwnership(Base): __tablename__ = 'car_ownerships' id = Column(Integer(), primary_key=True) type = Column(String(255)) car_id = Column(Integer(), ForeignKey(Car.id)) car = relationship('Car', backref=backref('car_ownerships', passive_deletes='all')) person_id = Column(Integer(), ForeignKey(Person.id)) person = relationship('Person', backref=backref('car_ownerships', passive_deletes='all')) def __repr__(self): return 'Ownership [{}]: {} <<-{}->> {}'.format(self.id, self.car, self.type, self.person)