summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/orm/session.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy/orm/session.py')
-rw-r--r--lib/sqlalchemy/orm/session.py319
1 files changed, 268 insertions, 51 deletions
diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py
index 6cb8a0062..8d2f13df3 100644
--- a/lib/sqlalchemy/orm/session.py
+++ b/lib/sqlalchemy/orm/session.py
@@ -12,6 +12,7 @@ import sys
import weakref
from . import attributes
+from . import context
from . import exc
from . import identity
from . import loading
@@ -28,13 +29,12 @@ from .base import state_str
from .unitofwork import UOWTransaction
from .. import engine
from .. import exc as sa_exc
-from .. import sql
+from .. import future
from .. import util
from ..inspection import inspect
from ..sql import coercions
from ..sql import roles
-from ..sql import util as sql_util
-
+from ..sql import visitors
__all__ = ["Session", "SessionTransaction", "sessionmaker"]
@@ -98,6 +98,160 @@ DEACTIVE = util.symbol("DEACTIVE")
CLOSED = util.symbol("CLOSED")
+class ORMExecuteState(object):
+ """Stateful object used for the :meth:`.SessionEvents.do_orm_execute`
+
+ .. versionadded:: 1.4
+
+ """
+
+ __slots__ = (
+ "session",
+ "statement",
+ "parameters",
+ "execution_options",
+ "bind_arguments",
+ )
+
+ def __init__(
+ self, session, statement, parameters, execution_options, bind_arguments
+ ):
+ self.session = session
+ self.statement = statement
+ self.parameters = parameters
+ self.execution_options = execution_options
+ self.bind_arguments = bind_arguments
+
+ def invoke_statement(
+ self,
+ statement=None,
+ params=None,
+ execution_options=None,
+ bind_arguments=None,
+ ):
+ """Execute the statement represented by this
+ :class:`.ORMExecuteState`, without re-invoking events.
+
+ This method essentially performs a re-entrant execution of the
+ current statement for which the :meth:`.SessionEvents.do_orm_execute`
+ event is being currently invoked. The use case for this is
+ for event handlers that want to override how the ultimate results
+ object is returned, such as for schemes that retrieve results from
+ an offline cache or which concatenate results from multiple executions.
+
+ :param statement: optional statement to be invoked, in place of the
+ statement currently represented by :attr:`.ORMExecuteState.statement`.
+
+ :param params: optional dictionary of parameters which will be merged
+ into the existing :attr:`.ORMExecuteState.parameters` of this
+ :class:`.ORMExecuteState`.
+
+ :param execution_options: optional dictionary of execution options
+ will be merged into the existing
+ :attr:`.ORMExecuteState.execution_options` of this
+ :class:`.ORMExecuteState`.
+
+ :param bind_arguments: optional dictionary of bind_arguments
+ which will be merged amongst the current
+ :attr:`.ORMExecuteState.bind_arguments`
+ of this :class:`.ORMExecuteState`.
+
+ :return: a :class:`_engine.Result` object with ORM-level results.
+
+ .. seealso::
+
+ :ref:`examples_caching` - includes example use of the
+ :meth:`.SessionEvents.do_orm_execute` hook as well as the
+ :meth:`.ORMExecuteState.invoke_query` method.
+
+
+ """
+
+ if statement is None:
+ statement = self.statement
+
+ _bind_arguments = dict(self.bind_arguments)
+ if bind_arguments:
+ _bind_arguments.update(bind_arguments)
+ _bind_arguments["_sa_skip_events"] = True
+
+ if params:
+ _params = dict(self.parameters)
+ _params.update(params)
+ else:
+ _params = self.parameters
+
+ if execution_options:
+ _execution_options = dict(self.execution_options)
+ _execution_options.update(execution_options)
+ else:
+ _execution_options = self.execution_options
+
+ return self.session.execute(
+ statement, _params, _execution_options, _bind_arguments
+ )
+
+ @property
+ def orm_query(self):
+ """Return the :class:`_orm.Query` object associated with this
+ execution.
+
+ For SQLAlchemy-2.0 style usage, the :class:`_orm.Query` object
+ is not used at all, and this attribute will return None.
+
+ """
+ load_opts = self.load_options
+ if load_opts._orm_query:
+ return load_opts._orm_query
+
+ opts = self._orm_compile_options()
+ if opts is not None:
+ return opts._orm_query
+ else:
+ return None
+
+ def _orm_compile_options(self):
+ opts = self.statement.compile_options
+ if isinstance(opts, context.ORMCompileState.default_compile_options):
+ return opts
+ else:
+ return None
+
+ @property
+ def loader_strategy_path(self):
+ """Return the :class:`.PathRegistry` for the current load path.
+
+ This object represents the "path" in a query along relationships
+ when a particular object or collection is being loaded.
+
+ """
+ opts = self._orm_compile_options()
+ if opts is not None:
+ return opts._current_path
+ else:
+ return None
+
+ @property
+ def load_options(self):
+ """Return the load_options that will be used for this execution."""
+
+ return self.execution_options.get(
+ "_sa_orm_load_options", context.QueryContext.default_load_options
+ )
+
+ @property
+ def user_defined_options(self):
+ """The sequence of :class:`.UserDefinedOptions` that have been
+ associated with the statement being invoked.
+
+ """
+ return [
+ opt
+ for opt in self.statement._with_options
+ if not opt._is_compile_state and not opt._is_legacy_option
+ ]
+
+
class SessionTransaction(object):
"""A :class:`.Session`-level transaction.
@@ -1032,9 +1186,7 @@ class Session(_SessionClassMethods):
def connection(
self,
- mapper=None,
- clause=None,
- bind=None,
+ bind_arguments=None,
close_with_result=False,
execution_options=None,
**kw
@@ -1059,23 +1211,18 @@ class Session(_SessionClassMethods):
resolved through any of the optional keyword arguments. This
ultimately makes usage of the :meth:`.get_bind` method for resolution.
+ :param bind_arguments: dictionary of bind arguments. may include
+ "mapper", "bind", "clause", other custom arguments that are passed
+ to :meth:`.Session.get_bind`.
+
:param bind:
- Optional :class:`_engine.Engine` to be used as the bind. If
- this engine is already involved in an ongoing transaction,
- that connection will be used. This argument takes precedence
- over ``mapper``, ``clause``.
+ deprecated; use bind_arguments
:param mapper:
- Optional :func:`.mapper` mapped class, used to identify
- the appropriate bind. This argument takes precedence over
- ``clause``.
+ deprecated; use bind_arguments
:param clause:
- A :class:`_expression.ClauseElement` (i.e.
- :func:`_expression.select`,
- :func:`_expression.text`,
- etc.) which will be used to locate a bind, if a bind
- cannot otherwise be identified.
+ deprecated; use bind_arguments
:param close_with_result: Passed to :meth:`_engine.Engine.connect`,
indicating the :class:`_engine.Connection` should be considered
@@ -1097,13 +1244,16 @@ class Session(_SessionClassMethods):
:ref:`session_transaction_isolation`
:param \**kw:
- Additional keyword arguments are sent to :meth:`get_bind()`,
- allowing additional arguments to be passed to custom
- implementations of :meth:`get_bind`.
+ deprecated; use bind_arguments
"""
+
+ if not bind_arguments:
+ bind_arguments = kw
+
+ bind = bind_arguments.pop("bind", None)
if bind is None:
- bind = self.get_bind(mapper, clause=clause, **kw)
+ bind = self.get_bind(**bind_arguments)
return self._connection_for_bind(
bind,
@@ -1124,7 +1274,14 @@ class Session(_SessionClassMethods):
conn = conn.execution_options(**execution_options)
return conn
- def execute(self, clause, params=None, mapper=None, bind=None, **kw):
+ def execute(
+ self,
+ statement,
+ params=None,
+ execution_options=util.immutabledict(),
+ bind_arguments=None,
+ **kw
+ ):
r"""Execute a SQL expression construct or string statement within
the current transaction.
@@ -1222,22 +1379,19 @@ class Session(_SessionClassMethods):
"executemany" will be invoked. The keys in each dictionary
must correspond to parameter names present in the statement.
+ :param bind_arguments: dictionary of additional arguments to determine
+ the bind. may include "mapper", "bind", or other custom arguments.
+ Contents of this dictionary are passed to the
+ :meth:`.Session.get_bind` method.
+
:param mapper:
- Optional :func:`.mapper` or mapped class, used to identify
- the appropriate bind. This argument takes precedence over
- ``clause`` when locating a bind. See :meth:`.Session.get_bind`
- for more details.
+ deprecated; use the bind_arguments dictionary
:param bind:
- Optional :class:`_engine.Engine` to be used as the bind. If
- this engine is already involved in an ongoing transaction,
- that connection will be used. This argument takes
- precedence over ``mapper`` and ``clause`` when locating
- a bind.
+ deprecated; use the bind_arguments dictionary
:param \**kw:
- Additional keyword arguments are sent to :meth:`.Session.get_bind()`
- to allow extensibility of "bind" schemes.
+ deprecated; use the bind_arguments dictionary
.. seealso::
@@ -1253,20 +1407,63 @@ class Session(_SessionClassMethods):
in order to execute the statement.
"""
- clause = coercions.expect(roles.CoerceTextStatementRole, clause)
- if bind is None:
- bind = self.get_bind(mapper, clause=clause, **kw)
+ statement = coercions.expect(roles.CoerceTextStatementRole, statement)
- return self._connection_for_bind(
- bind, close_with_result=True
- )._execute_20(clause, params,)
+ if not bind_arguments:
+ bind_arguments = kw
+ elif kw:
+ bind_arguments.update(kw)
+
+ compile_state_cls = statement._get_plugin_compile_state_cls("orm")
+ if compile_state_cls:
+ compile_state_cls.orm_pre_session_exec(
+ self, statement, execution_options, bind_arguments
+ )
+ else:
+ bind_arguments.setdefault("clause", statement)
+ if statement._is_future:
+ execution_options = util.immutabledict().merge_with(
+ execution_options, {"future_result": True}
+ )
+
+ if self.dispatch.do_orm_execute:
+ skip_events = bind_arguments.pop("_sa_skip_events", False)
+
+ if not skip_events:
+ orm_exec_state = ORMExecuteState(
+ self, statement, params, execution_options, bind_arguments
+ )
+ for fn in self.dispatch.do_orm_execute:
+ result = fn(orm_exec_state)
+ if result:
+ return result
+
+ bind = self.get_bind(**bind_arguments)
+
+ conn = self._connection_for_bind(bind, close_with_result=True)
+ result = conn._execute_20(statement, params or {}, execution_options)
- def scalar(self, clause, params=None, mapper=None, bind=None, **kw):
+ if compile_state_cls:
+ result = compile_state_cls.orm_setup_cursor_result(
+ self, bind_arguments, result
+ )
+
+ return result
+
+ def scalar(
+ self,
+ statement,
+ params=None,
+ execution_options=None,
+ mapper=None,
+ bind=None,
+ **kw
+ ):
"""Like :meth:`~.Session.execute` but return a scalar result."""
return self.execute(
- clause, params=params, mapper=mapper, bind=bind, **kw
+ statement, params=params, mapper=mapper, bind=bind, **kw
).scalar()
def close(self):
@@ -1422,7 +1619,7 @@ class Session(_SessionClassMethods):
"""
self._add_bind(table, bind)
- def get_bind(self, mapper=None, clause=None):
+ def get_bind(self, mapper=None, clause=None, bind=None):
"""Return a "bind" to which this :class:`.Session` is bound.
The "bind" is usually an instance of :class:`_engine.Engine`,
@@ -1497,6 +1694,8 @@ class Session(_SessionClassMethods):
:meth:`.Session.bind_table`
"""
+ if bind:
+ return bind
if mapper is clause is None:
if self.bind:
@@ -1520,6 +1719,8 @@ class Session(_SessionClassMethods):
raise
if self.__binds:
+ # matching mappers and selectables to entries in the
+ # binds dictionary; supported use case.
if mapper:
for cls in mapper.class_.__mro__:
if cls in self.__binds:
@@ -1528,18 +1729,32 @@ class Session(_SessionClassMethods):
clause = mapper.persist_selectable
if clause is not None:
- for t in sql_util.find_tables(clause, include_crud=True):
- if t in self.__binds:
- return self.__binds[t]
+ for obj in visitors.iterate(clause):
+ if obj in self.__binds:
+ return self.__binds[obj]
+ # session has a single bind; supported use case.
if self.bind:
return self.bind
- if isinstance(clause, sql.expression.ClauseElement) and clause.bind:
- return clause.bind
+ # now we are in legacy territory. looking for "bind" on tables
+ # that are via bound metadata. this goes away in 2.0.
+ if mapper and clause is None:
+ clause = mapper.persist_selectable
- if mapper and mapper.persist_selectable.bind:
- return mapper.persist_selectable.bind
+ if clause is not None:
+ if clause.bind:
+ return clause.bind
+ # for obj in visitors.iterate(clause):
+ # if obj.bind:
+ # return obj.bind
+
+ if mapper:
+ if mapper.persist_selectable.bind:
+ return mapper.persist_selectable.bind
+ # for obj in visitors.iterate(mapper.persist_selectable):
+ # if obj.bind:
+ # return obj.bind
context = []
if mapper is not None:
@@ -1722,9 +1937,11 @@ class Session(_SessionClassMethods):
else:
with_for_update = None
+ stmt = future.select(object_mapper(instance))
if (
loading.load_on_ident(
- self.query(object_mapper(instance)),
+ self,
+ stmt,
state.key,
refresh_state=state,
with_for_update=with_for_update,