"""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
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
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.
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
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):
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.
@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
-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
'before_commit',
'after_commit',
'after_rollback',
+ 'after_soft_rollback',
'before_flush',
'after_flush',
'after_flush_postexec',
'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