Auto Begin
~~~~~~~~~~
-.. versionadded:: 1.4
-
- This section describes a behavior that is new in SQLAlchemy 1.4 and does
- not apply to previous versions. Further details on the "autobegin"
- change are at :ref:`change_5074`.
-
The :class:`_orm.Session` object features a behavior known as **autobegin**.
This indicates that the :class:`_orm.Session` will internally consider itself
to be in a "transactional" state as soon as any work is performed with the
state unconditionally. :meth:`_orm.Session.begin` may be used as a context
manager as described at :ref:`session_begin_commit_rollback_block`.
-.. versionchanged:: 1.4.12 - autobegin now correctly occurs if object
- attributes are modified; previously this was not occurring.
+.. _session_autobegin_disable:
+
+Disabling Autobegin to Prevent Implicit Transactions
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The "autobegin" behavior may be disabled using the
+:paramref:`_orm.Session.autobegin` parameter set to ``False``. By using this
+parameter, a :class:`_orm.Session` will require that the
+:meth:`_orm.Session.begin` method is called explicitly. Upon construction, as
+well as after any of the :meth:`_orm.Session.rollback`,
+:meth:`_orm.Session.commit`, or :meth:`_orm.Session.close` methods are called,
+the :class:`_orm.Session` won't implicitly begin any new transactions and will
+raise an error if an attempt to use the :class:`_orm.Session` is made without
+first calling :meth:`_orm.Session.begin`::
+
+ with Session(engine, autobegin=False) as session:
+ session.begin() # <-- required, else InvalidRequestError raised on next call
+
+ session.add(User(name="u1"))
+ session.commit()
+
+ session.begin() # <-- required, else InvalidRequestError raised on next call
+
+ u1 = session.scalar(select(User).filter_by(name="u1"))
+.. versionadded:: 2.0 Added :paramref:`_orm.Session.autobegin`, allowing
+ "autobegin" behavior to be disabled
.. _session_committing:
from __future__ import annotations
import contextlib
+from enum import Enum
import itertools
import sys
import typing
]
+class SessionTransactionOrigin(Enum):
+ """indicates the origin of a :class:`.SessionTransaction`.
+
+ This enumeration is present on the
+ :attr:`.SessionTransaction.origin` attribute of any
+ :class:`.SessionTransaction` object.
+
+ .. versionadded:: 2.0
+
+ """
+
+ AUTOBEGIN = 0
+ """transaction were started by autobegin"""
+
+ BEGIN = 1
+ """transaction were started by calling :meth:`_orm.Session.begin`"""
+
+ BEGIN_NESTED = 2
+ """tranaction were started by :meth:`_orm.Session.begin_nested`"""
+
+ SUBTRANSACTION = 3
+ """transaction is an internal "subtransaction" """
+
+
class SessionTransaction(_StateChange, TransactionalContext):
"""A :class:`.Session`-level transaction.
InstanceState[Any], Tuple[Any, Any]
]
+ origin: SessionTransactionOrigin
+ """Origin of this :class:`_orm.SessionTransaction`.
+
+ Refers to a :class:`.SessionTransactionOrigin` instance which is an
+ enumeration indicating the source event that led to constructing
+ this :class:`_orm.SessionTransaction`.
+
+ .. versionadded:: 2.0
+
+ """
+
+ nested: bool = False
+ """Indicates if this is a nested, or SAVEPOINT, transaction.
+
+ When :attr:`.SessionTransaction.nested` is True, it is expected
+ that :attr:`.SessionTransaction.parent` will be present as well,
+ linking to the enclosing :class:`.SessionTransaction`.
+
+ .. seealso::
+
+ :attr:`.SessionTransaction.origin`
+
+ """
+
def __init__(
self,
session: Session,
+ origin: SessionTransactionOrigin,
parent: Optional[SessionTransaction] = None,
- nested: bool = False,
- autobegin: bool = False,
):
TransactionalContext._trans_ctx_check(session)
self.session = session
self._connections = {}
self._parent = parent
- self.nested = nested
+ self.nested = nested = origin is SessionTransactionOrigin.BEGIN_NESTED
+ self.origin = origin
+
if nested:
+ if not parent:
+ raise sa_exc.InvalidRequestError(
+ "Can't start a SAVEPOINT transaction when no existing "
+ "transaction is in progress"
+ )
+
self._previous_nested_transaction = session._nested_transaction
+ elif origin is SessionTransactionOrigin.SUBTRANSACTION:
+ assert parent is not None
+ else:
+ assert parent is None
+
self._state = SessionTransactionState.ACTIVE
- if not parent and nested:
- raise sa_exc.InvalidRequestError(
- "Can't start a SAVEPOINT transaction when no existing "
- "transaction is in progress"
- )
- self._take_snapshot(autobegin=autobegin)
+ self._take_snapshot()
# make sure transaction is assigned before we call the
# dispatch
"""
return self._parent
- nested: bool = False
- """Indicates if this is a nested, or SAVEPOINT, transaction.
-
- When :attr:`.SessionTransaction.nested` is True, it is expected
- that :attr:`.SessionTransaction.parent` will be True as well.
-
- """
-
@property
def is_active(self) -> bool:
return (
(SessionTransactionState.ACTIVE,), _StateChangeStates.NO_CHANGE
)
def _begin(self, nested: bool = False) -> SessionTransaction:
- return SessionTransaction(self.session, self, nested=nested)
+ return SessionTransaction(
+ self.session,
+ SessionTransactionOrigin.BEGIN_NESTED
+ if nested
+ else SessionTransactionOrigin.SUBTRANSACTION,
+ self,
+ )
def _iterate_self_and_parents(
self, upto: Optional[SessionTransaction] = None
return result
- def _take_snapshot(self, autobegin: bool = False) -> None:
+ def _take_snapshot(self) -> None:
if not self._is_transaction_boundary:
parent = self._parent
assert parent is not None
self._key_switches = parent._key_switches
return
- if not autobegin and not self.session._flushing:
+ is_begin = self.origin in (
+ SessionTransactionOrigin.BEGIN,
+ SessionTransactionOrigin.AUTOBEGIN,
+ )
+ if not is_begin and not self.session._flushing:
self.session.flush()
self._new = weakref.WeakKeyDictionary()
autoflush: bool = True,
future: Literal[True] = True,
expire_on_commit: bool = True,
+ autobegin: bool = True,
twophase: bool = False,
binds: Optional[Dict[_SessionBindKey, _SessionBind]] = None,
enable_baked_queries: bool = True,
:ref:`session_flushing` - additional background on autoflush
+ :param autobegin: Automatically start transactions (i.e. equivalent to
+ invoking :meth:`_orm.Session.begin`) when database access is
+ requested by an operation. Defaults to ``True``. Set to
+ ``False`` to prevent a :class:`_orm.Session` from implicitly
+ beginning transactions after construction, as well as after any of
+ the :meth:`_orm.Session.rollback`, :meth:`_orm.Session.commit`,
+ or :meth:`_orm.Session.close` methods are called.
+
+ .. versionadded:: 2.0
+
+ .. seealso::
+
+ :ref:`session_autobegin_disable`
+
:param bind: An optional :class:`_engine.Engine` or
:class:`_engine.Connection` to
which this ``Session`` should be bound. When specified, all SQL
self._transaction = None
self._nested_transaction = None
self.hash_key = _new_sessionid()
+ self.autobegin = autobegin
self.autoflush = autoflush
self.expire_on_commit = expire_on_commit
self.enable_baked_queries = enable_baked_queries
"""
return {}
- def _autobegin_t(self) -> SessionTransaction:
+ def _autobegin_t(self, begin: bool = False) -> SessionTransaction:
if self._transaction is None:
- trans = SessionTransaction(self, autobegin=True)
+ if not begin and not self.autobegin:
+ raise sa_exc.InvalidRequestError(
+ "Autobegin is disabled on this Session; please call "
+ "session.begin() to start a new transaction"
+ )
+ trans = SessionTransaction(
+ self,
+ SessionTransactionOrigin.BEGIN
+ if begin
+ else SessionTransactionOrigin.AUTOBEGIN,
+ )
assert self._transaction is trans
return trans
return self._transaction
- def begin(
- self, nested: bool = False, _subtrans: bool = False
- ) -> SessionTransaction:
+ def begin(self, nested: bool = False) -> SessionTransaction:
"""Begin a transaction, or nested transaction,
on this :class:`.Session`, if one is not already begun.
trans = self._transaction
if trans is None:
- trans = self._autobegin_t()
+ trans = self._autobegin_t(begin=True)
- if not nested and not _subtrans:
+ if not nested:
return trans
- if trans is not None:
- if _subtrans or nested:
- trans = trans._begin(nested=nested)
- assert self._transaction is trans
- if nested:
- self._nested_transaction = trans
- else:
- raise sa_exc.InvalidRequestError(
- "A transaction is already begun on this Session."
- )
- else:
- # outermost transaction. must be a not nested and not
- # a subtransaction
+ assert trans is not None
- assert not nested and not _subtrans
- trans = SessionTransaction(self)
+ if nested:
+ trans = trans._begin(nested=nested)
assert self._transaction is trans
-
- if TYPE_CHECKING:
- assert self._transaction is not None
+ self._nested_transaction = trans
+ else:
+ raise sa_exc.InvalidRequestError(
+ "A transaction is already begun on this Session."
+ )
return trans # needed for __enter__/__exit__ hook
if not flush_context.has_work:
return
- flush_context.transaction = transaction = self.begin(_subtrans=True)
+ flush_context.transaction = transaction = self._autobegin_t()._begin()
try:
self._warn_on_events = True
try:
mapper = _class_to_mapper(mapper)
self._flushing = True
- transaction = self.begin(_subtrans=True)
+ transaction = self._autobegin_t()._begin()
try:
if isupdate:
bulk_persistence._bulk_update(
assert not s.in_transaction()
eq_(s.connection().scalar(select(User.name)), "u1")
+ @testing.combinations(
+ "select1", "lazyload", "unitofwork", argnames="trigger"
+ )
+ @testing.combinations("commit", "close", "rollback", None, argnames="op")
+ def test_no_autobegin(self, op, trigger):
+ User, users = self.classes.User, self.tables.users
+ Address, addresses = self.classes.Address, self.tables.addresses
+
+ self.mapper_registry.map_imperatively(
+ User, users, properties={"addresses": relationship(Address)}
+ )
+ self.mapper_registry.map_imperatively(Address, addresses)
+
+ with Session(testing.db) as sess:
+
+ sess.add(User(name="u1"))
+ sess.commit()
+
+ s = Session(testing.db, autobegin=False)
+
+ orm_trigger = trigger == "lazyload" or trigger == "unitofwork"
+ with expect_raises_message(
+ sa.exc.InvalidRequestError,
+ r"Autobegin is disabled on this Session; please call "
+ r"session.begin\(\) to start a new transaction",
+ ):
+ if op or orm_trigger:
+ s.begin()
+
+ is_true(s.in_transaction())
+
+ if orm_trigger:
+ u1 = s.scalar(select(User).filter_by(name="u1"))
+ else:
+ eq_(s.scalar(select(1)), 1)
+
+ if op:
+ getattr(s, op)()
+ elif orm_trigger:
+ s.rollback()
+
+ is_false(s.in_transaction())
+
+ if trigger == "select1":
+ s.execute(select(1))
+ elif trigger == "lazyload":
+ if op == "close":
+ s.add(u1)
+ else:
+ u1.addresses
+ elif trigger == "unitofwork":
+ s.add(u1)
+
+ s.begin()
+ if trigger == "select1":
+ s.execute(select(1))
+ elif trigger == "lazyload":
+ if op == "close":
+ s.add(u1)
+ u1.addresses
+
+ is_true(s.in_transaction())
+
+ if op:
+ getattr(s, op)()
+ is_false(s.in_transaction())
+
def test_autobegin_begin_method(self):
s = Session(testing.db)
assert not sess.in_transaction()
sess.begin()
assert sess.is_active
- sess.begin(_subtrans=True)
+ sess._autobegin_t()._begin()
sess.rollback()
assert sess.is_active