Nesting of Transaction Blocks
-----------------------------
-.. note:: The "transaction nesting" feature of SQLAlchemy is a legacy feature
- that will be deprecated in an upcoming release. New usage paradigms will
- eliminate the need for it to be present.
+.. deprecated:: 1.4 The "transaction nesting" feature of SQLAlchemy is a legacy feature
+ that is deprecated in the 1.4 release and will be removed in SQLAlchemy 2.0.
+ The pattern has proven to be a little too awkward and complicated, unless an
+ application makes more of a first-class framework around the behavior. See
+ the following subsection :ref:`connections_avoid_nesting`.
The :class:`.Transaction` object also handles "nested" behavior by keeping
track of the outermost begin/commit pair. In this example, two functions both
available, but will automatically participate in an enclosing transaction if
one exists.
-.. index::
- single: thread safety; transactions
+.. _connections_avoid_nesting:
+
+Arbitrary Transaction Nesting as an Antipattern
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+With many years of experience, the above "nesting" pattern has not proven to
+be very popular, and where it has been observed in large projects such
+as Openstack, it tends to be complicated.
+
+The most ideal way to organize an application would have a single, or at
+least very few, points at which the "beginning" and "commit" of all
+database transactions is demarcated. This is also the general
+idea discussed in terms of the ORM at :ref:`session_faq_whentocreate`. To
+adapt the example from the previous section to this practice looks like::
+
+
+ # method_a calls method_b
+ def method_a(connection):
+ method_b(connection)
+
+ # method_b uses the connection and assumes the transaction
+ # is external
+ def method_b(connection):
+ connection.execute(text("insert into mytable values ('bat', 'lala')"))
+ connection.execute(mytable.insert(), {"col1": "bat", "col2": "lala"})
+
+ # open a Connection inside of a transaction and call method_a
+ with engine.begin() as conn:
+ method_a(conn)
+
+That is, ``method_a()`` and ``method_b()`` do not deal with the details
+of the transaction at all; the transactional scope of the connection is
+defined **externally** to the functions that have a SQL dialogue with the
+connection.
+
+It may be observed that the above code has fewer lines, and less indentation
+which tends to correlate with lower :term:`cyclomatic complexity`. The
+above code is organized such that ``method_a()`` and ``method_b()`` are always
+invoked from a point at which a transaction is begun. The previous
+version of the example features a ``method_a()`` and a ``method_b()`` that are
+trying to be agnostic of this fact, which suggests they are prepared for
+at least twice as many potential codepaths through them.
+
+.. _connections_subtransactions:
+
+Migrating from the "nesting" pattern
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+As SQLAlchemy's intrinsic-nested pattern is considered legacy, an application
+that for either legacy or novel reasons still seeks to have a context that
+automatically frames transactions should seek to maintain this functionality
+through the use of a custom Python context manager. A similar example is also
+provided in terms of the ORM in the "seealso" section below.
+
+To provide backwards compatibility for applications that make use of this
+pattern, the following context manager or a similar implementation based on
+a decorator may be used::
+
+ import contextlib
+
+ @contextlib.contextmanager
+ def transaction(connection):
+ if not connection.in_transaction():
+ with connection.begin():
+ yield connection
+ else:
+ yield connection
+
+The above contextmanager would be used as::
+
+ # method_a starts a transaction and calls method_b
+ def method_a(connection):
+ with transaction(connection): # open a transaction
+ method_b(connection)
+
+ # method_b either starts a transaction, or uses the one already
+ # present
+ def method_b(connection):
+ with transaction(connection): # open a transaction
+ connection.execute(text("insert into mytable values ('bat', 'lala')"))
+ connection.execute(mytable.insert(), {"col1": "bat", "col2": "lala"})
+
+ # open a Connection and call method_a
+ with engine.connect() as conn:
+ method_a(conn)
+
+A similar approach may be taken such that connectivity is established
+on demand as well; the below approach features a single-use context manager
+that accesses an enclosing state in order to test if connectivity is already
+present::
+
+ import contextlib
+
+ def connectivity(engine):
+ connection = None
+
+ @contextlib.contextmanager
+ def connect():
+ nonlocal connection
+
+ if connection is None:
+ connection = engine.connect()
+ with connection:
+ with connection.begin():
+ yield connection
+ else:
+ yield connection
+
+ return connect
+
+Using the above would look like::
+
+ # method_a passes along connectivity context, at the same time
+ # it chooses to establish a connection by calling "with"
+ def method_a(connectivity):
+ with connectivity():
+ method_b(connectivity)
+
+ # method_b also wants to use a connection from the context, so it
+ # also calls "with:", but also it actually uses the connection.
+ def method_b(connectivity):
+ with connectivity() as connection:
+ connection.execute(text("insert into mytable values ('bat', 'lala')"))
+ connection.execute(mytable.insert(), {"col1": "bat", "col2": "lala"})
+
+ # create a new connection/transaction context object and call
+ # method_a
+ method_a(connectivity(engine))
+
+The above context manager acts not only as a "transaction" context but also
+as a context that manages having an open connection against a particular
+:class:`_engine.Engine`. When using the ORM :class:`_orm.Session`, this
+connectivty management is provided by the :class:`_orm.Session` itself.
+An overview of ORM connectivity patterns is at :ref:`unitofwork_transaction`.
+
+.. seealso::
+
+ :ref:`session_subtransactions` - ORM version
.. _autocommit:
`Relational Algebra (via Wikipedia) <https://en.wikipedia.org/wiki/Relational_algebra>`_
+ cyclomatic complexity
+ A measure of code complexity based on the number of possible paths
+ through a program's source code.
+
+ .. seealso::
+
+ `Cyclomatic Complexity <https://en.wikipedia.org/wiki/Cyclomatic_complexity>`_
+
selectable
A term used in SQLAlchemy to describe a SQL construct that represents
a collection of rows. It's largely similar to the concept of a
.. versionchanged:: 1.4
SQLAlchemy 1.4 deprecates "autocommit mode", which is historically enabled
- by using the :paramref:`_orm.Session.autocommit` flag. This flag allows
- the :class:`_orm.Session` to invoke SQL statements within individual,
- ad-hoc transactions and has been recommended against for many years.
- Instead, the :meth:`_orm.Session.begin` method may now be called when
+ by using the :paramref:`_orm.Session.autocommit` flag. Going forward,
+ a new approach to allowing usage of the :meth:`_orm.Session.begin` method
+ is new "autobegin" behavior so that the method may now be called when
a :class:`_orm.Session` is first constructed, or after the previous
transaction has ended and before it begins a new one.
+ For background on migrating away from the "subtransaction" pattern for
+ frameworks that rely upon nesting of begin()/commit() pairs, see the
+ next section :ref:`session_subtransactions`.
+
The :class:`_orm.Session` features "autobegin" behavior, meaning that as soon
as operations begin to take place, it ensures a :class:`_orm.SessionTransaction`
is present to track ongoing operations. This transaction is completed
@contextlib.contextmanager
def transaction(session):
-
- if session.in_transaction():
- outermost = False
+ if not session.in_transaction():
+ with session.begin():
+ yield
else:
- outermost = True
- session.begin()
-
- try:
yield
- except:
- if session.in_transaction():
- session.rollback()
- raise
- else:
- if outermost and session.in_transaction():
- session.commit()
The above context manager may be used in the same way the
with transaction(session):
session.add(SomeObject('bat', 'lala'))
+ Session = sessionmaker(engine)
+
# create a Session and call method_a
with Session() as session:
method_a(session)
def method_b(session):
session.add(SomeObject('bat', 'lala'))
+ Session = sessionmaker(engine)
+
# create a Session and call method_a
with Session() as session:
with session.begin():
method_a(session)
+.. seealso::
+
+ :ref:`connections_subtransactions` - similar pattern based on Core only
.. _session_twophase:
@property
@util.deprecated_20(
- "The :attr:`_orm.Session.transaction` accessor is deprecated and "
- "will be removed in SQLAlchemy version 2.0. "
- "For context manager use, use :meth:`_orm.Session.begin`. To access "
+ ":attr:`_orm.Session.transaction`",
+ alternative="For context manager use, use "
+ ":meth:`_orm.Session.begin`. To access "
"the current root transaction, use "
- ":meth:`_orm.Session.get_transaction`"
+ ":meth:`_orm.Session.get_transaction`.",
)
def transaction(self):
"""The current active or inactive :class:`.SessionTransaction`.
mapper(Address, addresses)
-class SubtransactionRecipeTest(FixtureTest):
- run_inserts = None
- __backend__ = True
+def subtransaction_recipe_one(self):
+ @contextlib.contextmanager
+ def transaction(session):
- future = False
-
- @testing.fixture
- def subtransaction_recipe(self):
- @contextlib.contextmanager
- def transaction(session):
+ if session.in_transaction():
+ outermost = False
+ else:
+ outermost = True
+ session.begin()
+ try:
+ yield
+ except:
if session.in_transaction():
- outermost = False
- else:
- outermost = True
- session.begin()
+ session.rollback()
+ raise
+ else:
+ if outermost and session.in_transaction():
+ session.commit()
+ return transaction
+
+
+def subtransaction_recipe_two(self):
+ # shorter recipe
+ @contextlib.contextmanager
+ def transaction(session):
+ if not session.in_transaction():
+ with session.begin():
+ yield
+ else:
+ yield
+
+ return transaction
+
+
+def subtransaction_recipe_three(self):
+ @contextlib.contextmanager
+ def transaction(session):
+ if not session.in_transaction():
+ session.begin()
try:
yield
except:
if session.in_transaction():
session.rollback()
- raise
else:
- if outermost and session.in_transaction():
- session.commit()
+ session.commit()
+ else:
+ try:
+ yield
+ except:
+ if session.in_transaction():
+ session.rollback()
+ raise
+
+ return transaction
+
- return transaction
+@testing.combinations(
+ (subtransaction_recipe_one, True),
+ (subtransaction_recipe_two, False),
+ (subtransaction_recipe_three, True),
+ argnames="target_recipe,recipe_rollsback_early",
+ id_="ns",
+)
+@testing.combinations((True,), (False,), argnames="future", id_="s")
+class SubtransactionRecipeTest(FixtureTest):
+ run_inserts = None
+ __backend__ = True
+
+ @testing.fixture
+ def subtransaction_recipe(self):
+ return self.target_recipe()
@testing.requires.savepoints
def test_recipe_heavy_nesting(self, subtransaction_recipe):
except:
pass
- # that was a real rollback, so no transaction
- is_(sess.get_transaction(), None)
+ if self.recipe_rollsback_early:
+ # that was a real rollback, so no transaction
+ assert not sess.in_transaction()
+ is_(sess.get_transaction(), None)
+ else:
+ assert sess.in_transaction()
sess.close()
+ assert not sess.in_transaction()
def test_recipe_multi_nesting(self, subtransaction_recipe):
sess = Session(testing.db, future=self.future)
except:
pass
- assert not sess.in_transaction()
+ if self.recipe_rollsback_early:
+ assert not sess.in_transaction()
+ else:
+ assert sess.in_transaction()
+
+ assert not sess.in_transaction()
def test_recipe_deactive_status_check(self, subtransaction_recipe):
sess = Session(testing.db, future=self.future)
sess.commit() # no error
-class FutureSubtransactionRecipeTest(SubtransactionRecipeTest):
- future = True
-
-
class FixtureDataTest(_LocalFixture):
run_inserts = "each"
__backend__ = True