Eliminar hijos después de eliminar padre en SQLAlchemy

Mi problema es el siguiente:

Tengo los dos modelos Entry y Tag vinculados por una relación de muchos a muchos en SQLAlchemy. Ahora quiero eliminar todas las Tag que no tengan ninguna Entry correspondiente después de eliminar una Entry .

Ejemplo para ilustrar lo que quiero:

Con estas dos entradas, la base de datos contiene las tags python , java y c++ . Si ahora elimino la Entry 2 , quiero que SQLAlchemy elimine automáticamente la etiqueta c++ de la base de datos. ¿Es posible definir este comportamiento en el modelo Entry o existe una forma aún más elegante?

Gracias.

esta pregunta se hizo hace un tiempo atrás aquí: establecer delete-huérfano en la relación SQLAlchemy causa AssertionError: este atributo no está configurado para rastrear a los padres

Este es el problema de “muchos a muchos huérfanos”. jadkik94 está cerca en que deberías usar eventos para detectar esto, pero trato de recomendar que no se use la sesión dentro de los eventos del asignador, aunque funciona en este caso.

A continuación, tomo la respuesta textualmente de la otra pregunta de SO y sustituyo la palabra “Función” por “Entrada”:

 from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy import * from sqlalchemy.orm import * from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import event from sqlalchemy.orm import attributes Base= declarative_base() tagging = Table('tagging',Base.metadata, Column('tag_id', Integer, ForeignKey('tag.id', ondelete='cascade'), primary_key=True), Column('entry_id', Integer, ForeignKey('entry.id', ondelete='cascade'), primary_key=True) ) class Tag(Base): __tablename__ = 'tag' id = Column(Integer, primary_key=True) name = Column(String(100), unique=True, nullable=False) def __init__(self, name=None): self.name = name class Entry(Base): __tablename__ = 'entry' id = Column(Integer, primary_key=True) tag_names = association_proxy('tags', 'name') tags = relationship('Tag', secondary=tagging, backref='entries') @event.listens_for(Session, 'after_flush') def delete_tag_orphans(session, ctx): # optional: look through Session state to see if we want # to emit a DELETE for orphan Tags flag = False for instance in session.dirty: if isinstance(instance, Entry) and \ attributes.get_history(instance, 'tags').deleted: flag = True break for instance in session.deleted: if isinstance(instance, Entry): flag = True break # emit a DELETE for all orphan Tags. This is safe to emit # regardless of "flag", if a less verbose approach is # desired. if flag: session.query(Tag).\ filter(~Tag.entries.any()).\ delete(synchronize_session=False) e = create_engine("sqlite://", echo=True) Base.metadata.create_all(e) s = Session(e) r1 = Entry() r2 = Entry() r3 = Entry() t1, t2, t3, t4 = Tag("t1"), Tag("t2"), Tag("t3"), Tag("t4") r1.tags.extend([t1, t2]) r2.tags.extend([t2, t3]) r3.tags.extend([t4]) s.add_all([r1, r2, r3]) assert s.query(Tag).count() == 4 r2.tags.remove(t2) assert s.query(Tag).count() == 4 r1.tags.remove(t2) assert s.query(Tag).count() == 3 r1.tags.remove(t1) assert s.query(Tag).count() == 2 

dos preguntas de SO casi idénticas califican esto como algo para tener en la mano, así que lo he agregado a la wiki en http://www.sqlalchemy.org/trac/wiki/UsageRecipes/ManyToManyOrphan .

Dejaré que el código hable por mí:

 from sqlalchemy import create_engine, exc, event from sqlalchemy.orm import sessionmaker from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import func, Table, Column, Integer, String, Float, Boolean, MetaData, ForeignKey from sqlalchemy.orm import relationship, backref # Connection engine = create_engine('sqlite:///', echo=True) Base = declarative_base(bind=engine) Session = sessionmaker(bind=engine) # Models entry_tag_link = Table('entry_tag', Base.metadata, Column('entry_id', Integer, ForeignKey('entries.id')), Column('tag_id', Integer, ForeignKey('tags.id')) ) class Entry(Base): __tablename__ = 'entries' id = Column(Integer, primary_key=True) name = Column(String(255), nullable=False, default='') tags = relationship("Tag", secondary=entry_tag_link, backref="entries") def __repr__(self): return '' % (self.name,) class Tag(Base): __tablename__ = 'tags' id = Column(Integer, primary_key=True) name = Column(String(255), nullable=False) def __repr__(self): return '' % (self.name,) # Delete listener def delete_listener(mapper, connection, target): print "---- DELETING %s ----" % (target,) print '-' * 20 for t in target.tags: if len(t.entries) == 0: print ' ' * 5, t, 'is to be deleted' session.delete(t) print '-' * 20 event.listen(Entry, 'before_delete', delete_listener) # Utility functions def dump(session): entries = session.query(Entry).all() tags = session.query(Tag).all() print '*' * 20 print 'Entries', entries print 'Tags', tags print '*' * 20 Base.metadata.create_all() session = Session() t1, t2, t3 = Tag(name='python'), Tag(name='java'), Tag(name='c++') e1, e2 = Entry(name='Entry 1', tags=[t1, t2]), Entry(name='Entry 2', tags=[t1, t3]) session.add_all([e1,e2]) session.commit() dump(session) raw_input("---- Press return to delete the second entry and see the result ----") session.delete(e2) session.commit() dump(session) 

Este código anterior utiliza el evento after_delete de los eventos ORM de SQLAlchemy. Esta línea hace la magia:

 event.listen(Entry, 'before_delete', delete_listener) 

Esto dice que escuchar todas las eliminaciones de un elemento de Entry , y llamar a nuestro oyente que hará lo que queremos. Sin embargo, los documentos no recomiendan cambiar la sesión dentro de los eventos (vea la advertencia en el enlace que agregué). Pero por lo que puedo ver, funciona, así que depende de usted ver si esto funciona para usted.