From: Mike Bayer Date: Mon, 1 Aug 2011 18:16:46 +0000 (-0400) Subject: - Added after_soft_rollback() Session event. This X-Git-Tag: rel_0_7_3~103 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=195a26e2fb5cd8c24381e467f94a14577c756843;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - 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] - SessionTransaction is mentioned in public docs, many more docstrings for events etc. otherwise --- diff --git a/CHANGES b/CHANGES index ec66db0c15..ff74044c49 100644 --- 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 diff --git a/doc/build/orm/session.rst b/doc/build/orm/session.rst index 517d043d79..5dd5c09096 100644 --- a/doc/build/orm/session.rst +++ b/doc/build/orm/session.rst @@ -1533,6 +1533,9 @@ Session and sessionmaker() .. autoclass:: sqlalchemy.orm.session.Session :members: +.. autoclass:: sqlalchemy.orm.session.SessionTransaction + :members: + Session Utilites ---------------- diff --git a/lib/sqlalchemy/__init__.py b/lib/sqlalchemy/__init__.py index cf7ebd22cd..c15cc6b693 100644 --- a/lib/sqlalchemy/__init__.py +++ b/lib/sqlalchemy/__init__.py @@ -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 diff --git a/lib/sqlalchemy/orm/events.py b/lib/sqlalchemy/orm/events.py index 8c12e72b1e..a8162fa723 100644 --- a/lib/sqlalchemy/orm/events.py +++ b/lib/sqlalchemy/orm/events.py @@ -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. diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 700d843dd7..0810fa31fd 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -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 diff --git a/test/orm/test_events.py b/test/orm/test_events.py index 0dc73627f7..6e5a676f4e 100644 --- a/test/orm/test_events.py +++ b/test/orm/test_events.py @@ -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