Feature Request: scoped_session() should not rely on anything to do the cleanup

Issue #1084 resolved
Former user created an issue

It would be awesome if scoped_session() returned a proxy that behaved pretty much like Spring JDBC/Hibernate/ORM templates :

Basically, those templates count the number of getSession() that are issued, and the number of commit()/rollback().

a getSession returns the threadlocal's one if already present, or creates a new one(which scoped_session does OK), and increases the getSession Count

commit has no effect if the getSession count > 1, except decreasing the getSession count.

commit does the real commit is getSession count == 1, and completly cleansup the thread local, which scoped_session doesn't do (it relies on something else to call the cleanup method)

This allows completly decoupling layers...

Comments (4)

  1. Mike Bayer repo owner

    our session.begin()/commit() already does this (its called a "subtransaction") as does the begin()/commit() method on Connection. you can call begin() as many times as needed and only the outermost "commit()" actually commits. This is all in the docs for both (see http://www.sqlalchemy.org/docs/05/sqlalchemy_orm_session.html#docstrings_sqlalchemy.orm.session_Session. and http://www.sqlalchemy.org/docs/05/sqlalchemy_engine.html#docstrings_sqlalchemy.engine_Connection).

    You can easily provide a wrapper around scoped_session() which returns a Session with autocommit=True and explicitly calls begin(subtransactions=True) to achieve this nesting.

    In particular, the best way to do this in Python which does not generally have Springlike dependency injection or EJB containers is via decorators:

    Session = scoped_session(sessionmaker(autocommit=True))
    
    def transactional(fn):
        def do(self):
            session = Session()
            session.begin()
            try:
                fn(self, session)
                session.commit()
            except:
                session.rollback()
                raise
        return do
    
    class MyClass(object):
       @transactional
       def foobar(self, session):
           # do something with the session
    

    I work with Hibernate/Spring all day long and I think the level of magic inherent in those toolkits (such as, if you declare a threadlocal session in your hibernate.cfg.xml and also use a Spring context manager, silent, sporadic failure. The same if you accidentally configure two different Spring context managers....) is inappropriate for Python, which isn't relying upon XML configuration files and favors explicitness over implicitness. So we make it plenty easy to build that pattern yourself as illustrated. Frameworks such as Turbogears and Grok decide upon these patterns in a similar fashion as Spring does for Hibernate.

  2. Mike Bayer repo owner

    that should read:

    def transactional(fn):
        def do(self):
            session = Session()
            session.begin(subtransactions=True)
            try:
                fn(self, session)
                session.commit()
            except:
                session.rollback()
                raise
        return do
    
  3. Former user Account Deleted

    I'm impressed that the begin(subtransaction) trick is implemented, but this behavior was not obvious to me by reading the documentation (and I did read it...). thank you in any case !

    Anyways, the decorator you provided does not do any cleanup (Session.remove()), so here is a short implementation of a template that works, if anybody seeing this bug report is interested...

    To completly mimic spring's behaviour, I guess self.__local should be made a module-level variable instead, ...

    class _SessionTemplate(object):
        """ Simple helper class akin to Spring-JDBC/Hibernate/ORM Template.
        It doesnt't commit nor releases resources if other do_with_session() calls are pending """
        def __init__(self, sessionmaker):
            self.__sessionmaker = sessionmaker
            self.__local = threading.local()
    
        def do_with_session(self, session_callback):        
            session = self.__sessionmaker()
            self.__local.do_with_session_count = self.__local.do_with_session_count+1 if hasattr(self.__local,'do_with_session_count') and self.__local.do_with_session_count is not None else 1   
            o = session_callback(session)
            count = self.__local.do_with_session_count
            if count == 1:
                session.commit()
                session.close()
                self.__sessionmaker.remove()
                del self.__local.do_with_session_count
            else:
                self.__local.do_with_session_count = count-1
            return o
    
  4. Mike Bayer repo owner

    I think the stuff with the counter could be better implemented by just looking to see if the current tranasaction is active ( I think this is what TG does):

    session.commit()
    if not session.is_active:
        sessionmaker.remove()
    
  5. Log in to comment