]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Added after_soft_rollback() Session event. This
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 1 Aug 2011 18:16:46 +0000 (14:16 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 1 Aug 2011 18:16:46 +0000 (14:16 -0400)
     event fires unconditionally whenever rollback()
     is called, regardless of if an actual DBAPI
     level rollback occurred.  This event
    is specifically designed to allow operations
     with the Session to proceed after a rollback
     when the Session.is_active is True.
     [ticket:2241]
- SessionTransaction is mentioned in public docs, many
more docstrings for events etc. otherwise

CHANGES
doc/build/orm/session.rst
lib/sqlalchemy/__init__.py
lib/sqlalchemy/orm/events.py
lib/sqlalchemy/orm/session.py
test/orm/test_events.py

diff --git a/CHANGES b/CHANGES
index ec66db0c15333bc717815ac7c417aacc018b0754..ff74044c498a72f2846afe90a68f4fee2a352a68 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -3,6 +3,18 @@
 =======
 CHANGES
 =======
+0.7.3
+=====
+- orm
+   - Added after_soft_rollback() Session event.  This
+     event fires unconditionally whenever rollback()
+     is called, regardless of if an actual DBAPI
+     level rollback occurred.  This event
+     is specifically designed to allow operations
+     with the Session to proceed after a rollback
+     when the Session.is_active is True.
+     [ticket:2241]
+
 0.7.2
 =====
 - orm
index 517d043d79470c6d114a84219d7cc06f3df36373..5dd5c09096427649c3e2cf7bec7d71b5bdc3fead 100644 (file)
@@ -1533,6 +1533,9 @@ Session and sessionmaker()
 .. autoclass:: sqlalchemy.orm.session.Session
    :members:
 
+.. autoclass:: sqlalchemy.orm.session.SessionTransaction
+   :members:
+
 Session Utilites
 ----------------
 
index cf7ebd22cdac694796c286492e21dd72d220bc75..c15cc6b693de2accf109401466c3b9596444d9ab 100644 (file)
@@ -117,6 +117,6 @@ from sqlalchemy.engine import create_engine, engine_from_config
 __all__ = sorted(name for name, obj in locals().items()
                  if not (name.startswith('_') or inspect.ismodule(obj)))
 
-__version__ = '0.7.2'
+__version__ = '0.7.3'
 
 del inspect, sys
index 8c12e72b1ef21c637a5049195aae1dd2be08caef..a8162fa723686c9739905b002e4b404bb014976c 100644 (file)
@@ -821,25 +821,82 @@ class SessionEvents(event.Events):
         """Execute before commit is called.
 
         Note that this may not be per-flush if a longer running
-        transaction is ongoing."""
+        transaction is ongoing.
+
+        :param session: The target :class:`.Session`.
+        
+        """
 
     def after_commit(self, session):
         """Execute after a commit has occurred.
 
         Note that this may not be per-flush if a longer running
-        transaction is ongoing."""
+        transaction is ongoing.
+        
+        :param session: The target :class:`.Session`.
+        
+        """
 
     def after_rollback(self, session):
-        """Execute after a rollback has occurred.
+        """Execute after a real DBAPI rollback has occurred.
+        
+        Note that this event only fires when the *actual* rollback against
+        the database occurs - it does *not* fire each time the 
+        :meth:`.Session.rollback` method is called, if the underlying 
+        DBAPI transaction has already been rolled back.  In many
+        cases, the :class:`.Session` will not be in 
+        an "active" state during this event, as the current
+        transaction is not valid.   To acquire a :class:`.Session`
+        which is active after the outermost rollback has proceeded,
+        use the :meth:`.SessionEvents.after_soft_rollback` event, checking the
+        :attr:`.Session.is_active` flag.
+
+        :param session: The target :class:`.Session`.
 
-        Note that this may not be per-flush if a longer running
-        transaction is ongoing."""
+        """
+
+    def after_soft_rollback(self, session, previous_transaction):
+        """Execute after any rollback has occurred, including "soft" 
+        rollbacks that don't actually emit at the DBAPI level.
+        
+        This corresponds to both nested and outer rollbacks, i.e.
+        the innermost rollback that calls the DBAPI's 
+        rollback() method, as well as the enclosing rollback 
+        calls that only pop themselves from the transaction stack.
+        
+        The given :class:`.Session` can be used to invoke SQL and 
+        :meth:`.Session.query` operations after an outermost rollback 
+        by first checking the :attr:`.Session.is_active` flag::
+
+            @event.listens_for(Session, "after_soft_rollback")
+            def do_something(session, previous_transaction):
+                if session.is_active:
+                    session.execute("select * from some_table")
+        
+        :param session: The target :class:`.Session`.
+        :param previous_transaction: The :class:`.SessionTransaction` transactional
+         marker object which was just closed.   The current :class:`.SessionTransaction`
+         for the given :class:`.Session` is available via the
+         :attr:`.Session.transaction` attribute.
+
+        New in 0.7.3.
+
+        """
 
     def before_flush( self, session, flush_context, instances):
         """Execute before flush process has started.
 
         `instances` is an optional list of objects which were passed to
-        the ``flush()`` method. """
+        the ``flush()`` method. 
+        
+        :param session: The target :class:`.Session`.
+        :param flush_context: Internal :class:`.UOWTransaction` object
+         which handles the details of the flush.
+        :param instances: Usually ``None``, this is the collection of
+         objects which can be passed to the :meth:`.Session.flush` method
+         (note this usage is deprecated).
+
+        """
 
     def after_flush(self, session, flush_context):
         """Execute after flush has completed, but before commit has been
@@ -847,7 +904,13 @@ class SessionEvents(event.Events):
 
         Note that the session's state is still in pre-flush, i.e. 'new',
         'dirty', and 'deleted' lists still show pre-flush state as well
-        as the history settings on instance attributes."""
+        as the history settings on instance attributes.
+        
+        :param session: The target :class:`.Session`.
+        :param flush_context: Internal :class:`.UOWTransaction` object
+         which handles the details of the flush.
+
+        """
 
     def after_flush_postexec(self, session, flush_context):
         """Execute after flush has completed, and after the post-exec
@@ -856,13 +919,25 @@ class SessionEvents(event.Events):
         This will be when the 'new', 'dirty', and 'deleted' lists are in
         their final state.  An actual commit() may or may not have
         occurred, depending on whether or not the flush started its own
-        transaction or participated in a larger transaction. """
+        transaction or participated in a larger transaction. 
+        
+        :param session: The target :class:`.Session`.
+        :param flush_context: Internal :class:`.UOWTransaction` object
+         which handles the details of the flush.
+        """
 
     def after_begin( self, session, transaction, connection):
         """Execute after a transaction is begun on a connection
 
         `transaction` is the SessionTransaction. This method is called
-        after an engine level transaction is begun on a connection. """
+        after an engine level transaction is begun on a connection. 
+        
+        :param session: The target :class:`.Session`.
+        :param transaction: The :class:`.SessionTransaction`.
+        :param connection: The :class:`~.engine.base.Connection` object 
+         which will be used for SQL statements.
+        
+        """
 
     def after_attach(self, session, instance):
         """Execute after an instance is attached to a session.
index 700d843dd77d76e1a3eb4c3c437847d4d4babea7..0810fa31fd0c60424ba517760672d56ae9e3cf03 100644 (file)
@@ -105,15 +105,35 @@ def sessionmaker(bind=None, class_=None, autoflush=True, autocommit=False,
 class SessionTransaction(object):
     """A Session-level transaction.
 
-    This corresponds to one or more :class:`~sqlalchemy.engine.Transaction`
-    instances behind the scenes, with one ``Transaction`` per ``Engine`` in
-    use.
+    This corresponds to one or more Core :class:`~.engine.base.Transaction`
+    instances behind the scenes, with one :class:`~.engine.base.Transaction`
+    per :class:`~.engine.base.Engine` in use.
+
+    Direct usage of :class:`.SessionTransaction` is not typically 
+    necessary as of SQLAlchemy 0.4; use the :meth:`.Session.rollback` and 
+    :meth:`.Session.commit` methods on :class:`.Session` itself to 
+    control the transaction.
+    
+    The current instance of :class:`.SessionTransaction` for a given
+    :class:`.Session` is available via the :attr:`.Session.transaction`
+    attribute.
 
-    Direct usage of ``SessionTransaction`` is not necessary as of SQLAlchemy
-    0.4; use the ``begin()`` and ``commit()`` methods on ``Session`` itself.
+    The :class:`.SessionTransaction` object is **not** thread-safe.
 
-    The ``SessionTransaction`` object is **not** thread-safe.
+    See also:
+    
+    :meth:`.Session.rollback`
+    
+    :meth:`.Session.commit`
 
+    :attr:`.Session.is_active`
+    
+    :meth:`.SessionEvents.after_commit`
+    
+    :meth:`.SessionEvents.after_rollback`
+    
+    :meth:`.SessionEvents.after_soft_rollback`
+    
     .. index::
       single: thread safety; SessionTransaction
 
@@ -321,9 +341,14 @@ class SessionTransaction(object):
                 else:
                     transaction._deactivate()
 
+        sess = self.session
+
         self.close()
         if self._parent and _capture_exception:
             self._parent._rollback_exception = sys.exc_info()[1]
+
+        sess.dispatch.after_soft_rollback(sess, self)
+
         return self._parent
 
     def _rollback_impl(self):
@@ -528,6 +553,9 @@ class Session(object):
 
     connection_callable = None
 
+    transaction = None
+    """The current active or inactive :class:`.SessionTransaction`."""
+
     def begin(self, subtransactions=False, nested=False):
         """Begin a transaction on this Session.
 
@@ -1648,7 +1676,19 @@ class Session(object):
 
     @property
     def is_active(self):
-        """True if this Session has an active transaction."""
+        """True if this :class:`.Session` has an active transaction.
+        
+        This indicates if the :class:`.Session` is capable of emitting
+        SQL, as from the :meth:`.Session.execute`, :meth:`.Session.query`,
+        or :meth:`.Session.flush` methods.   If False, it indicates 
+        that the innermost transaction has been rolled back, but enclosing
+        :class:`.SessionTransaction` objects remain in the transactional
+        stack, which also must be rolled back.
+        
+        This flag is generally only useful with a :class:`.Session`
+        configured in its default mode of ``autocommit=False``.
+
+        """
 
         return self.transaction and self.transaction.is_active
 
index 0dc73627f7d29029bd728cc70c18bb16baeb0a27..6e5a676f4e6c20ba7722879e2f7c8c9cd84797e0 100644 (file)
@@ -1,4 +1,4 @@
-from test.lib.testing import assert_raises_message
+from test.lib.testing import assert_raises_message, assert_raises
 import sqlalchemy as sa
 from test.lib import testing
 from sqlalchemy import Integer, String
@@ -537,6 +537,7 @@ class SessionEventsTest(_RemoveListeners, _fixtures.FixtureTest):
             'before_commit',
             'after_commit',
             'after_rollback',
+            'after_soft_rollback',
             'before_flush',
             'after_flush',
             'after_flush_postexec',
@@ -567,6 +568,55 @@ class SessionEventsTest(_RemoveListeners, _fixtures.FixtureTest):
             'before_commit', 'after_commit',]
         )
 
+    def test_rollback_hook(self):
+        User, users = self.classes.User, self.tables.users
+        sess, canary = self._listener_fixture()
+        mapper(User, users)
+
+        u = User(name='u1', id=1)
+        sess.add(u)
+        sess.commit()
+
+        u2 = User(name='u1', id=1)
+        sess.add(u2)
+        assert_raises(
+            sa.orm.exc.FlushError,
+            sess.commit
+        )
+        sess.rollback()
+        eq_(canary, ['after_attach', 'before_commit', 'before_flush', 
+        'after_begin', 'after_flush', 'after_flush_postexec', 
+        'after_commit', 'after_attach', 'before_commit', 
+        'before_flush', 'after_begin', 'after_rollback', 
+        'after_soft_rollback', 'after_soft_rollback'])
+
+    def test_can_use_session_in_outer_rollback_hook(self):
+        User, users = self.classes.User, self.tables.users
+        mapper(User, users)
+
+        sess = Session()
+
+        assertions = []
+        @event.listens_for(sess, "after_soft_rollback")
+        def do_something(session, previous_transaction):
+            if session.is_active:
+                assertions.append('name' not in u.__dict__)
+                assertions.append(u.name == 'u1')
+
+        u = User(name='u1', id=1)
+        sess.add(u)
+        sess.commit()
+
+        u2 = User(name='u1', id=1)
+        sess.add(u2)
+        assert_raises(
+            sa.orm.exc.FlushError,
+            sess.commit
+        )
+        sess.rollback()
+        eq_(assertions, [True, True])
+
+
     def test_flush_noautocommit_hook(self):
         User, users = self.classes.User, self.tables.users