Hello, I looked closely at Query implementation and I ended up with this:
from sqlalchemy.orm.query import Query import sqlalchemy.event as event class DeletedAtQuery(Query): """ Query adding a default condition that, if deleted_at exists, it should be NULL for the rows returned """ def get(self, ident): return Query.get(self.populate_existing(), ident) def __iter__(self): return Query.__iter__(self.private()) def from_self(self, *ent): return Query.from_self(self.private(), *ent) def private(self): mzero = self._mapper_zero() if mzero is not None: if hasattr(mzero.class_, 'deleted_at'): crit = mzero.class_.deleted_at == None return self.filter(crit) return self def delete(self, synchronize_session='evaluate'): context = self._compile_context() assert len(context.statement.froms) == 1 if 'deleted_at' in context.statement.froms[0].columns: self.update({'deleted_at': int(time.time())}, synchronize_session=synchronize_session) return super(DeletedAtQuery, self).\ delete(synchronize_session=synchronize_session) def paranoid_delete(session, flush_context, instances): """ Intercepts item deletions (single item deletions) and, if there is a field deleted_at for that object, it's updating deleted at instead of really deleting the object """ for obj in session.deleted: if hasattr(obj, 'deleted_at'): print "moving object back: ", obj obj.deleted_at = int(time.time()) session.add(obj) def configure_events(session): """ Helper for applying before_flush hook """ event.listen(session, 'before_flush', paranoid_delete) I'm using _compile_context private method, and I would like to know if it's anything I could improve in order to get to a complete implementation of safe delete. Thank you, Andrei Chirila On Tue, Sep 20, 2011 at 4:18 PM, Michael Bayer <mike...@zzzcomputing.com>wrote: > > On Sep 20, 2011, at 10:12 AM, Andrei Chirila wrote: > > Hello Michael, > > Thank you :) > > I changed the code according to your indications: > > def paranoid_delete(session, flush_context, instances): > """ Intercepts item deletions (single item deletions) > and, if there is a field deleted_at for that > object, it's updating deleted at instead of really > deleting the object """ > # print "current session", session > # print "context", flush_context > # print "dirty", session.dirty > # print "deleted", session._deleted > for obj in session.deleted: > # print ">> deleted object:", instance, " -> ", obj > if hasattr(obj, 'deleted_at'): > print "moving object back: ", obj > obj.deleted_at = int(time.time()) > session.add(obj) > > and it seems to do what it was intended. > > Do you know where could I add a hook for before_bulk_delete? > > > Well since you're subclassing Query I'd add an overriding delete() method > to your LimitingQuery, do your hook in there, then call the delete() method > of Query itself. > > > > > Thank you, > Andrei Chirila > > > On Tue, Sep 20, 2011 at 3:59 PM, Michael Bayer > <mike...@zzzcomputing.com>wrote: > >> >> On Sep 20, 2011, at 8:33 AM, Andrei Chirila wrote: >> >> > Hello, >> > >> > I'm working at implementing a safe delete on my models (having a >> > deleted_at field which is checked and setting a deleted_at field when >> > a model is deleted). >> > >> > I looked over the LimitingQuery recipe and at >> > SessionEvents.before_flush and bellow is my solution. As I'm kind of >> > new SqlAlchemy user, I would like to ask if anyone can see any obvious >> > mistake in the following code (I'm sure I missed bulk deletion and >> > session explunge, but I don't really know how to integrate them). >> >> I'd stay away from identity_map.add(object), as that skips a lot of >> bookkeeping that occurs within the Session. and definitely *never* modify >> anything with an _underscore on Session, your code becomes totally non >> portable to even small changes in SQLAlchemy (which occur frequently as we >> have new releases, bugfixes you'll want, etc). The public (read-only) >> accessor for the "deleted" collection is Session.deleted. >> >> If you have an object marked for deletion, you can re-add it, >> session.add(object), will remove it from the deleted list. >> >> If there's something you need the Session to do that there is not a public >> API for, ask me on the list here first, as I'd rather ensure there are >> maintainable paths for use cases. >> >> >> >> >> > >> > Any feedback would be appreciated. >> > >> > Thank you, >> > Andrei Chirila >> > >> > from sqlalchemy.orm.query import Query >> > import sqlalchemy.event as event >> > >> > >> > class DeletedAtQuery(Query): >> > """ Query adding a default condition that, >> > if deleted_at exists, it should be NULL >> > for the rows returned """ >> > >> > def get(self, ident): >> > return Query.get(self.populate_existing(), ident) >> > >> > def __iter__(self): >> > return Query.__iter__(self.private()) >> > >> > def from_self(self, *ent): >> > return Query.from_self(self.private(), *ent) >> > >> > def private(self): >> > mzero = self._mapper_zero() >> > if mzero is not None: >> > if hasattr(mzero.class_, 'deleted_at'): >> > crit = mzero.class_.deleted_at == None >> > >> > return self.filter(crit) >> > return self >> > >> > >> > def paranoid_delete(session, flush_context, instances): >> > """ Intercepts item deletions (single item deletions) >> > and, if there is a field deleted_at for that >> > object, it's updating deleted at instead of really >> > deleting the object """ >> > # print "current session", session >> > # print "context", flush_context >> > # print "dirty", session.dirty >> > # print "deleted", session._deleted >> > deleted_copy = set(session._deleted) >> > for instance in deleted_copy: >> > obj = instance.obj() >> > # print ">> deleted object:", instance, " -> ", obj >> > if hasattr(obj, 'deleted_at'): >> > # print "moving instance to dirty states" >> > instance.deleted = False >> > obj.deleted_at = int(time.time()) >> > session.identity_map.add(instance) >> > del session._deleted[instance] >> > >> > >> > def configure_events(session): >> > """ Helper for applying before_flush hook """ >> > event.listen(session, 'before_flush', paranoid_delete) >> > >> > if __name__ == "__main__": >> > >> > import time >> > from sqlalchemy import * >> > from sqlalchemy.orm import * >> > from sqlalchemy.ext.declarative import declarative_base >> > >> > Base = declarative_base() >> > >> > class Group(Base): >> > __tablename__ = 'groups' >> > >> > id = Column(Integer, primary_key=True) >> > name = Column(String) >> > users = relation("User", backref="group", cascade="all") >> > >> > def __repr__(self): >> > return "<Group(%d, %s)>" % (self.id, self.name) >> > >> > class User(Base): >> > __tablename__ = 'users' >> > >> > id = Column(Integer, primary_key=True) >> > name = Column(String) >> > group_id = Column(Integer, ForeignKey('groups.id')) >> > deleted_at = Column(Integer) >> > pets = relation("Pet", backref="user", cascade="all, delete- >> > orphan") >> > >> > def __repr__(self): >> > return "<User(%d, %s)>" % (self.id, self.name) >> > >> > class Pet(Base): >> > __tablename__ = 'pets' >> > >> > id = Column(Integer, primary_key=True) >> > name = Column(String) >> > user_id = Column(Integer, ForeignKey('users.id'), >> > nullable=False) >> > deleted_at = Column(Integer) >> > >> > def __repr__(self): >> > return "<Pet(%d, %s)>" % (self.id, self.name) >> > >> > engine = create_engine("sqlite://", echo=False) >> > Base.metadata.create_all(engine) >> > >> > ClearSession = sessionmaker(bind=engine) >> > Session = sessionmaker(bind=engine, query_cls=DeletedAtQuery) >> > >> > sess = Session() >> > configure_events(sess) >> > clearsess = ClearSession() >> > >> > sess.add_all([Group(name='Group A', users=[ >> > User(name='User A1', pets=[ >> > Pet(name='Mause A1'), >> > Pet(name='Cat A1')])]), >> > Group(name='Group B', users=[ >> > User(name='User B1'), >> > User(name='User B2', pets=[ >> > Pet(name='Dog B2'), >> > Pet(name='Mause B2'), >> > Pet(name='Hamster B2') >> > ]), >> > User(name='User B3', pets=[ >> > Pet(name='Fish B3'), >> > Pet(name='Snake B3'), >> > Pet(name='Horse B3') >> > ])])]) >> > >> > sess.commit() >> > >> > group_a = sess.query(Group).filter(Group.name == 'Group >> > A').first() >> > assert group_a >> > >> > users_in_a = group_a.users >> > assert users_in_a >> > assert len(users_in_a) == 1 >> > users_in_a_by_query = sess.query(User).\ >> > filter(User.group_id == group_a.id).\ >> > all() >> > >> > assert users_in_a_by_query >> > assert len(users_in_a_by_query) == 1 >> > >> > pets_in_a = users_in_a[0].pets >> > assert pets_in_a >> > assert len(pets_in_a) == 2 >> > >> > # delete one pet >> > pet_id = pets_in_a[0].id >> > sess.delete(pets_in_a[0]) >> > sess.commit() >> > >> > pets_in_a = users_in_a[0].pets >> > assert pets_in_a >> > assert len(pets_in_a) == 1 >> > >> > pet = clearsess.query(Pet).filter(Pet.id == pet_id).first() >> > assert pet >> > assert pet.deleted_at >> > >> > sess.delete(users_in_a[0]) >> > sess.commit() >> > >> > assert len(sess.query(User).\ >> > filter(User.group_id == group_a.id).\ >> > all()) == 0 >> > assert len(clearsess.query(User).\ >> > filter(User.group_id == group_a.id).\ >> > all()) == 1 >> > assert len(sess.query(Pet).all()) == 6 >> > assert len(clearsess.query(Pet).all()) == 8 >> > >> > group_b = sess.query(Group).filter(Group.name == 'Group >> > B').first() >> > assert group_b >> > assert len(group_b.users) == 3 >> > >> > # don't know how to implement batch delete hook yet >> > #sess.query(Pet).delete() >> > #sess.commit() >> > >> > #assert len(sess.query(Pet).all()) == 0 >> > #assert len(clearsess.query(Pet).all()) == 8 >> > >> > sess.delete(group_b) >> > sess.commit() >> > groub_b_deleted = sess.query(Group).filter(Group.name == 'Group >> > B').first() >> > assert groub_b_deleted is None >> > >> > assert len(sess.query(Pet).all()) == 0 >> > assert len(clearsess.query(Pet).all()) == 8 >> > assert len(sess.query(User).all()) == 0 >> > assert len(clearsess.query(User).all()) == 4 >> > >> > -- >> > You received this message because you are subscribed to the Google >> Groups "sqlalchemy" group. >> > To post to this group, send email to sqlalchemy@googlegroups.com. >> > To unsubscribe from this group, send email to >> sqlalchemy+unsubscr...@googlegroups.com. >> > For more options, visit this group at >> http://groups.google.com/group/sqlalchemy?hl=en. >> > >> >> -- >> You received this message because you are subscribed to the Google Groups >> "sqlalchemy" group. >> To post to this group, send email to sqlalchemy@googlegroups.com. >> To unsubscribe from this group, send email to >> sqlalchemy+unsubscr...@googlegroups.com. >> For more options, visit this group at >> http://groups.google.com/group/sqlalchemy?hl=en. >> >> > > > -- > Mobile: +49 151 42647382 > e-Mail: andre...@gmail.com > > -- > You received this message because you are subscribed to the Google Groups > "sqlalchemy" group. > To post to this group, send email to sqlalchemy@googlegroups.com. > To unsubscribe from this group, send email to > sqlalchemy+unsubscr...@googlegroups.com. > For more options, visit this group at > http://groups.google.com/group/sqlalchemy?hl=en. > > > -- > You received this message because you are subscribed to the Google Groups > "sqlalchemy" group. > To post to this group, send email to sqlalchemy@googlegroups.com. > To unsubscribe from this group, send email to > sqlalchemy+unsubscr...@googlegroups.com. > For more options, visit this group at > http://groups.google.com/group/sqlalchemy?hl=en. > -- Mobile: +49 151 42647382 e-Mail: andre...@gmail.com -- You received this message because you are subscribed to the Google Groups "sqlalchemy" group. To post to this group, send email to sqlalchemy@googlegroups.com. To unsubscribe from this group, send email to sqlalchemy+unsubscr...@googlegroups.com. For more options, visit this group at http://groups.google.com/group/sqlalchemy?hl=en.