diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2022-10-11 17:01:43 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2022-10-11 19:05:41 -0400 |
| commit | 4580239b35642045c847c6faac8dd4fe304bb845 (patch) | |
| tree | 3558ab9d11852a289b69ee424fe6d5fc3fc89f0c /lib/sqlalchemy/orm | |
| parent | d4b8b41832267918e74c114c26def5b38d15e9e2 (diff) | |
| download | sqlalchemy-4580239b35642045c847c6faac8dd4fe304bb845.tar.gz | |
implement autobegin=False option
Added new parameter :paramref:`_orm.Session.autobegin`, which when set to
``False`` will prevent the :class:`_orm.Session` from beginning a
transaction implicitly. The :meth:`_orm.Session.begin` method must be
called explicitly first in order to proceed with operations, otherwise an
error is raised whenever any operation would otherwise have begun
automatically. This option can be used to create a "safe"
:class:`_orm.Session` that won't implicitly start new transactions.
As part of this change, also added a new status variable
:class:`_orm.SessionTransaction.origin` which may be useful for event
handling code to be aware of the origin of a particular
:class:`_orm.SessionTransaction`.
Fixes: #6928
Change-Id: I246f895c4a475bff352216e5bc74b6a25e6a4ae7
Diffstat (limited to 'lib/sqlalchemy/orm')
| -rw-r--r-- | lib/sqlalchemy/orm/__init__.py | 1 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/scoping.py | 11 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/session.py | 166 |
3 files changed, 127 insertions, 51 deletions
diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index c6b61f3b4..5e2161515 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -127,6 +127,7 @@ from .session import ORMExecuteState as ORMExecuteState from .session import Session as Session from .session import sessionmaker as sessionmaker from .session import SessionTransaction as SessionTransaction +from .session import SessionTransactionOrigin as SessionTransactionOrigin from .state import AttributeState as AttributeState from .state import InstanceState as InstanceState from .strategy_options import contains_eager as contains_eager diff --git a/lib/sqlalchemy/orm/scoping.py b/lib/sqlalchemy/orm/scoping.py index e3ba86c5a..1971caedc 100644 --- a/lib/sqlalchemy/orm/scoping.py +++ b/lib/sqlalchemy/orm/scoping.py @@ -382,9 +382,7 @@ class scoped_session(Generic[_S]): return self._proxied.add_all(instances) - def begin( - self, nested: bool = False, _subtrans: bool = False - ) -> SessionTransaction: + def begin(self, nested: bool = False) -> SessionTransaction: r"""Begin a transaction, or nested transaction, on this :class:`.Session`, if one is not already begun. @@ -425,7 +423,7 @@ class scoped_session(Generic[_S]): """ # noqa: E501 - return self._proxied.begin(nested=nested, _subtrans=_subtrans) + return self._proxied.begin(nested=nested) def begin_nested(self) -> SessionTransaction: r"""Begin a "nested" transaction on this Session, e.g. SAVEPOINT. @@ -1772,6 +1770,11 @@ class scoped_session(Generic[_S]): .. versionadded:: 1.4.24 + .. seealso:: + + :ref:`orm_queryguide_select_orm_entities` - contrasts the behavior + of :meth:`_orm.Session.execute` to :meth:`_orm.Session.scalars` + """ # noqa: E501 diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index c759f6541..22e47585d 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -10,6 +10,7 @@ from __future__ import annotations import contextlib +from enum import Enum import itertools import sys import typing @@ -734,6 +735,30 @@ class ORMExecuteState(util.MemoizedSlots): ] +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. @@ -790,29 +815,60 @@ class SessionTransaction(_StateChange, TransactionalContext): 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 @@ -866,14 +922,6 @@ class SessionTransaction(_StateChange, TransactionalContext): """ 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 ( @@ -901,7 +949,13 @@ class SessionTransaction(_StateChange, TransactionalContext): (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 @@ -923,7 +977,7 @@ class SessionTransaction(_StateChange, TransactionalContext): 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 @@ -933,7 +987,11 @@ class SessionTransaction(_StateChange, TransactionalContext): 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() @@ -1307,6 +1365,7 @@ class Session(_SessionClassMethods, EventTarget): 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, @@ -1330,6 +1389,20 @@ class Session(_SessionClassMethods, EventTarget): :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 @@ -1455,6 +1528,7 @@ class Session(_SessionClassMethods, EventTarget): 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 @@ -1542,18 +1616,26 @@ class Session(_SessionClassMethods, EventTarget): """ 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. @@ -1590,31 +1672,21 @@ class Session(_SessionClassMethods, EventTarget): 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 @@ -3957,7 +4029,7 @@ class Session(_SessionClassMethods, EventTarget): 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: @@ -4246,7 +4318,7 @@ class Session(_SessionClassMethods, EventTarget): 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( |
