Question

I have a new question about SQLAlchemy and I broke my brain while try to find a good solution. So I've a some tables:

import sqlalchemy.orm.session

# other import statments . . .

Session = sqlalchemy.orm.session.Session

class Tempable(Base):
    id = Column(Integer, primary_key=True)
    name = Column(String, nullable=False, unique=True)
    temporary = Column(Boolean, nullable=False)

class Generic(Base):
    id = Column(Integer, primary_key=True)
    name = Column(String, nullable=False, unique=True)
    tempable_id = Column(Integer, ForeignKey(Tempable.id))

Tempable table has a field named temporary. When this field is True then only one Generic can be relate to this Tempable table row and when related Generic row deleted then Tempable must also be deleted. Otherwise many Generic can be connected with Tempable and removing one of them don't affect to Tempable. After some researches I've figured out that must convient way to do this is using of events. Code expanded to the follows:

class Generic(Base):
    # . . .
    def before_delete(self, session):
        """:type session: Session"""
        condition = and_(Tempable.id == self.tempable_id, Tempable.temporary == 1)
        # I've tried use bulk session deletion:
        # session.query(Tempable).filter(condition).delete()
        # but if Tempable tables has relationships then related objects not deleted, 
        # I don't understand such behaviour.
        # But this works fine:
        for obj in session.query(Tempable).filter(condition):
            session.delete(obj)

@event.listens_for(Session, 'before_flush')
def _database_flush(session, flush_context, instances):
    for p_object in session.deleted:
        if hasattr(p_object, "before_delete"):
            p_object.before_delete(session)
    for p_object in session.dirty:
        if hasattr(p_object, "before_update"):
            p_object.before_update(session)
    for p_object in session.new:
        if hasattr(p_object, "before_insert"):
            p_object.before_insert(session)

But some troubles occurred. When Generic object deleted the corresponding GUI must be also updated. For this purpose deleted property of Session object can be used. But also there is a problem for me: deleted Tempable row not appeared in this property list.

class Database(object):

    # . . .

    def remove(name):
        # before commit I need to obtain list of all objects that will be deleted
        # that required to update GUI views

        try:
            this = self.__session.query(orm.Generic).filter(orm.Generic.name == name).one()
        except orm.NoResultFound:
            pass
        else:
            logging.info("Remove object: %s" % this)
            self.__session.delete(this)

        deleted = [obj for obj in self.__session.deleted]

        # At this point, list of deleted objects of course is not contain any Tempable objects
        print(deleted)

        self.__session.commit()

        # And here list is empty
        print([obj for obj in self.__session.deleted])

        return deleted

So the question what is right way to do obtain deleted objects or may be the whole approach is totally wrong?

Was it helpful?

Solution

The bulk delete system does not handle your relationships because it emits a single DELETE statement for all rows, without attempting to load and reconcile what those rows refer to. This is the first "Caveat" listed in the documentation for query.delete():

The method does not offer in-Python cascading of relationships - it is assumed that ON DELETE CASCADE/SET NULL/etc. is configured for any foreign key references which require it, otherwise the database may emit an integrity violation if foreign key references are being enforced.

as far as "session.deleted", that list is only relevant before the flush occurs. commit() implies flush() and after that flush, all of session.new, session.dirty, session.deleted are cleared. You would need to either copy session.deleted before it is cleared to another list, or probably more robust is to gather up session.deleted within before_flush() as you're doing, and copy those objects which you care about to another list, perhaps within session.info; e.g. session.info['deleted_things'] = my_list_of_objects.

OTHER TIPS

This is my implementation of what I asked for that based on @zzzeek helpful answer. First I've override Session.delete method. In this method if being deleted instance is Generic and DeleteHook classes instance then on_delete method called before sqlalchemy.orm.session.Session.delete method called. The main caveat here was that if sqlalchemy.orm.session.Session.delete method is execute in on_delete then after the last sqlalchemy.orm.session.Session.delete method called session.deleted will be reset. So I made a workaround: on_delete method should return query to objects that must be also be deleted. Then Session.delete at last step performed removing of all objects with autoflush off.

I think code can clear it out:

import sqlalchemy.orm.session
import sqlalchemy.orm.query

# other import statments . . .

Query = sqlalchemy.orm.query.Query

# Interface of Generic objects that should perform operations before deleted
class DeleteHook(object):

    def on_delete(self, session):
        """
        Hook performed before inherited Generic object marked for deletion. 
        It can return query of others object that also required to delete.

        :param Session session: Current database session
        :return: Query of the objects to being delete
        :rtype: Query
        """
        return []


class Session(sqlalchemy.orm.session.Session):

    def delete(self, instance):
        """
        Override delete method of the Session to execute on_delete 
        method of instance if required
        """
        deleted = [instance]
        if isinstance(instance, Generic) and isinstance(instance, DeleteHook):
            deleted.extend(instance.on_delete(self))
        # Autoflush required to be off otherwise 
        # if object with relationships deleted then
        # session.deleted will be reset
        self.autoflush = False
        # All removed objects collected in the session.deleted set
        for p_object in deleted:
            super(Session, self).delete(p_object)
        self.autoflush = True


class Tempable(Base):
    id = Column(Integer, primary_key=True)
    name = Column(String, nullable=False, unique=True)
    temporary = Column(Boolean, nullable=False)


class Generic(Base, DeleteHook):

    id = Column(Integer, primary_key=True)
    name = Column(String, nullable=False, unique=True)
    tempable_id = Column(Integer, ForeignKey(Tempable.id))

    def on_delete(self, session):
        """
        :param Session session: Current database session
        :return: Query of the objects to being delete
        :rtype: Query
        """
        # Possible some others statements can be placed here
        # . . .
        # Select only temporary records
        condition = and_(Tempable.id == self.tempable_id, 
                         Tempable.temporary == 1)
        return session.query(Tempable).filter(condition)
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top