diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2020-05-25 22:36:44 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2020-05-28 14:38:56 -0400 |
commit | 77f1b7d236dba6b1c859bb428ef32d118ec372e6 (patch) | |
tree | 7fae8eaaf303d6ce02bd423abf216550001e2f7b /lib/sqlalchemy/orm/session.py | |
parent | 366e88ea0e5c5417184c1dd4776cff752560631d (diff) | |
download | sqlalchemy-77f1b7d236dba6b1c859bb428ef32d118ec372e6.tar.gz |
callcount reductions and refinement for cached queries
This commit includes that we've removed the "_orm_query"
attribute from compile state as well as query context.
The attribute created reference cycles and also added
method call overhead. As part of this change,
the interface for ORMExecuteState changes a bit, as well
as the interface for the horizontal sharding extension
which now deprecates the "query_chooser" callable
in favor of "execute_chooser", which receives the contextual
object. This will also work more nicely when we implement
the new execution path for bulk updates and deletes.
Pre-merge execution options for statement, connection,
arguments all up front in Connection. that way they
can be passed to the before_execute / after_execute events,
and the ExecutionContext doesn't have to merge as second
time. Core execute is pretty close to 1.3 now.
baked wasn't using the new one()/first()/one_or_none() methods,
fixed that.
Convert non-buffered cursor strategy to be a stateless
singleton. inline all the paths by which the strategy
gets chosen, oracle and SQL Server dialects make use of the
already-invoked post_exec() hook to establish the alternate
strategies, and this is actually much nicer than it was before.
Add caching to mapper instance processor for getters.
Identified a reference cycle per query that was showing
up as a lot of gc cleanup, fixed that.
After all that, performance not budging much. Even
test_baked_query now runs with significantly fewer function
calls than 1.3, still 40% slower.
Basically something about the new patterns just makes
this slower and while I've walked a whole bunch of them
back, it hardly makes a dent. that said, the performance
issues are relatively small, in the 20-40% time increase
range, and the new caching feature
does provide for regular ORM and Core queries that
are cached, and they are faster than non-cached.
Change-Id: I7b0b0d8ca550c05f79e82f75cd8eff0bbfade053
Diffstat (limited to 'lib/sqlalchemy/orm/session.py')
-rw-r--r-- | lib/sqlalchemy/orm/session.py | 106 |
1 files changed, 77 insertions, 29 deletions
diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 8d2f13df3..25e224348 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -35,6 +35,7 @@ from ..inspection import inspect from ..sql import coercions from ..sql import roles from ..sql import visitors +from ..sql.base import CompileState __all__ = ["Session", "SessionTransaction", "sessionmaker"] @@ -98,7 +99,7 @@ DEACTIVE = util.symbol("DEACTIVE") CLOSED = util.symbol("CLOSED") -class ORMExecuteState(object): +class ORMExecuteState(util.MemoizedSlots): """Stateful object used for the :meth:`.SessionEvents.do_orm_execute` .. versionadded:: 1.4 @@ -109,7 +110,8 @@ class ORMExecuteState(object): "session", "statement", "parameters", - "execution_options", + "_execution_options", + "_merged_execution_options", "bind_arguments", ) @@ -119,7 +121,7 @@ class ORMExecuteState(object): self.session = session self.statement = statement self.parameters = parameters - self.execution_options = execution_options + self._execution_options = execution_options self.bind_arguments = bind_arguments def invoke_statement( @@ -182,33 +184,51 @@ class ORMExecuteState(object): _params = self.parameters if execution_options: - _execution_options = dict(self.execution_options) + _execution_options = dict(self._execution_options) _execution_options.update(execution_options) else: - _execution_options = self.execution_options + _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. + def execution_options(self): + """Placeholder for execution options. + + Raises an informative message, as there are local options + vs. merged options that can be viewed, via the + :attr:`.ORMExecuteState.local_execution_options` and + :attr:`.ORMExecuteState.merged_execution_options` methods. - 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 + raise AttributeError( + "Please use .local_execution_options or " + ".merged_execution_options" + ) - opts = self._orm_compile_options() - if opts is not None: - return opts._orm_query - else: - return None + @property + def local_execution_options(self): + """Dictionary view of the execution options passed to the + :meth:`.Session.execute` method. This does not include options + that may be associated with the statement being invoked. + + """ + return util.immutabledict(self._execution_options) + + @property + def merged_execution_options(self): + """Dictionary view of all execution options merged together; + this includes those of the statement as well as those passed to + :meth:`.Session.execute`, with the local options taking precedence. + + """ + return self._merged_execution_options + + def _memoized_attr__merged_execution_options(self): + return self.statement._execution_options.union(self._execution_options) def _orm_compile_options(self): opts = self.statement.compile_options @@ -218,6 +238,21 @@ class ORMExecuteState(object): return None @property + def lazy_loaded_from(self): + """An :class:`.InstanceState` that is using this statement execution + for a lazy load operation. + + The primary rationale for this attribute is to support the horizontal + sharding extension, where it is available within specific query + execution time hooks created by this extension. To that end, the + attribute is only intended to be meaningful at **query execution + time**, and importantly not any time prior to that, including query + compilation time. + + """ + return self.load_options._lazy_loaded_from + + @property def loader_strategy_path(self): """Return the :class:`.PathRegistry` for the current load path. @@ -235,7 +270,7 @@ class ORMExecuteState(object): def load_options(self): """Return the load_options that will be used for this execution.""" - return self.execution_options.get( + return self._execution_options.get( "_sa_orm_load_options", context.QueryContext.default_load_options ) @@ -1407,7 +1442,6 @@ class Session(_SessionClassMethods): in order to execute the statement. """ - statement = coercions.expect(roles.CoerceTextStatementRole, statement) if not bind_arguments: @@ -1415,12 +1449,19 @@ class Session(_SessionClassMethods): elif kw: bind_arguments.update(kw) - compile_state_cls = statement._get_plugin_compile_state_cls("orm") - if compile_state_cls: + if ( + statement._propagate_attrs.get("compile_state_plugin", None) + == "orm" + ): + compile_state_cls = CompileState._get_plugin_class_for_plugin( + statement, "orm" + ) + compile_state_cls.orm_pre_session_exec( self, statement, execution_options, bind_arguments ) else: + compile_state_cls = None bind_arguments.setdefault("clause", statement) if statement._is_future: execution_options = util.immutabledict().merge_with( @@ -1694,9 +1735,19 @@ class Session(_SessionClassMethods): :meth:`.Session.bind_table` """ + + # this function is documented as a subclassing hook, so we have + # to call this method even if the return is simple if bind: return bind + elif not self.__binds and self.bind: + # simplest and most common case, we have a bind and no + # per-mapper/table binds, we're done + return self.bind + # we don't have self.bind and either have self.__binds + # or we don't have self.__binds (which is legacy). Look at the + # mapper and the clause if mapper is clause is None: if self.bind: return self.bind @@ -1707,6 +1758,7 @@ class Session(_SessionClassMethods): "a binding." ) + # look more closely at the mapper. if mapper is not None: try: mapper = inspect(mapper) @@ -1718,6 +1770,7 @@ class Session(_SessionClassMethods): else: raise + # match up the mapper or clause in the __binds if self.__binds: # matching mappers and selectables to entries in the # binds dictionary; supported use case. @@ -1733,7 +1786,8 @@ class Session(_SessionClassMethods): if obj in self.__binds: return self.__binds[obj] - # session has a single bind; supported use case. + # none of the __binds matched, but we have a fallback bind. + # return that if self.bind: return self.bind @@ -1745,16 +1799,10 @@ class Session(_SessionClassMethods): 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: |