summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/ext/baked.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy/ext/baked.py')
-rw-r--r--lib/sqlalchemy/ext/baked.py137
1 files changed, 119 insertions, 18 deletions
diff --git a/lib/sqlalchemy/ext/baked.py b/lib/sqlalchemy/ext/baked.py
index a32154d21..65d6a8603 100644
--- a/lib/sqlalchemy/ext/baked.py
+++ b/lib/sqlalchemy/ext/baked.py
@@ -1,3 +1,18 @@
+# sqlalchemy/ext/baked.py
+# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+"""Baked query extension.
+
+Provides a creational pattern for the :class:`.query.Query` object which
+allows the fully constructed object, Core select statement, and string
+compiled result to be fully cached.
+
+
+"""
+
from ..orm.query import Query
from ..orm import strategies, attributes, properties, \
strategy_options, util as orm_util, interfaces
@@ -14,11 +29,11 @@ log = logging.getLogger(__name__)
class BakedQuery(object):
- _global_bakery = util.LRUCache(1000)
+ """A builder object for :class:`.query.Query` objects."""
__slots__ = 'steps', '_bakery', '_cache_key', '_spoiled'
- def __init__(self, initial_fn, args=(), bakery=None):
+ def __init__(self, bakery, initial_fn, args=()):
if args:
self._cache_key = tuple(args)
else:
@@ -26,10 +41,18 @@ class BakedQuery(object):
self._update_cache_key(initial_fn)
self.steps = [initial_fn]
self._spoiled = False
- if bakery is not None:
- self._bakery = bakery
- else:
- self._bakery = self._global_bakery
+ self._bakery = bakery
+
+ @classmethod
+ def bakery(cls, size=200):
+ """Construct a new bakery."""
+
+ _bakery = util.LRUCache(size)
+
+ def call(initial_fn):
+ return cls(_bakery, initial_fn)
+
+ return call
def _clone(self):
b1 = BakedQuery.__new__(BakedQuery)
@@ -56,33 +79,71 @@ class BakedQuery(object):
return self.with_criteria(other)
def add_criteria(self, fn, *args):
+ """Add a criteria function to this :class:`.BakedQuery`.
+
+ This is equivalent to using the ``+=`` operator to
+ modify a :class:`.BakedQuery` in-place.
+
+ """
self._update_cache_key(fn, args)
self.steps.append(fn)
return self
def with_criteria(self, fn, *args):
+ """Add a criteria function to a :class:`.BakedQuery` cloned from this one.
+
+ This is equivalent to using the ``+`` operator to
+ produce a new :class:`.BakedQuery` with modifications.
+
+ """
return self._clone().add_criteria(fn, *args)
def for_session(self, session):
+ """Return a :class:`.Result` object for this :class:`.BakedQuery`.
+
+ This is equivalent to calling the :class:`.BakedQuery` as a
+ Python callable, e.g. ``result = my_baked_query(session)``.
+
+ """
return Result(self, session)
def __call__(self, session):
return self.for_session(session)
- def spoil(self):
+ def spoil(self, full=False):
"""Cancel any query caching that will occur on this BakedQuery object.
- The BakedQuery can continue to be used normally, however when it
- actually iterates results, no caching will be used.
+ The BakedQuery can continue to be used normally, however additional
+ creational functions will not be cached; they will be called
+ on every invocation.
This is to support the case where a particular step in constructing
a baked query disqualifies the query from being cacheable, such
as a variant that relies upon some uncacheable value.
+ :param full: if False, only functions added to this
+ :class:`.BakedQuery` object subsequent to the spoil step will be
+ non-cached; the state of the :class:`.BakedQuery` up until
+ this point will be pulled from the cache. If True, then the
+ entire :class:`.Query` object is built from scratch each
+ time, with all creational functions being called on each
+ invocation.
+
"""
+ if not full:
+ _spoil_point = self._clone()
+ _spoil_point._cache_key += ('_query_only', )
+ self.steps = [_spoil_point._retrieve_baked_query]
self._spoiled = True
return self
+ def _retrieve_baked_query(self, session):
+ query = self._bakery.get(self._cache_key, None)
+ if query is None:
+ query = self._as_query(session)
+ self._bakery[self._cache_key] = query.with_session(None)
+ return query.with_session(session)
+
def _bake(self, session):
query = self._as_query(session)
@@ -101,6 +162,7 @@ class BakedQuery(object):
'_joinpath', '_joinpoint'):
query.__dict__.pop(attr, None)
self._bakery[self._cache_key] = context
+ return context
def _as_query(self, session):
query = self.steps[0](session)
@@ -122,7 +184,7 @@ class BakedQuery(object):
for k, v in list(context.attributes.items()):
if isinstance(v, Query):
if 'subquery' in k:
- bk = BakedQuery(lambda *args: v)
+ bk = BakedQuery(self._bakery, lambda *args: v)
bk._cache_key = self._cache_key + k
bk._bake(session)
baked_queries.append((k, bk._cache_key, v))
@@ -135,12 +197,19 @@ class BakedQuery(object):
"""
for k, cache_key, query in context.attributes["baked_queries"]:
- bk = BakedQuery(lambda sess: query.with_session(sess))
+ bk = BakedQuery(self._bakery, lambda sess: query.with_session(sess))
bk._cache_key = cache_key
context.attributes[k] = bk.for_session(session).params(**params)
class Result(object):
+ """Invokes a :class:`.BakedQuery` against a :class:`.Session`.
+
+ The :class:`.Result` object is where the actual :class:`.query.Query`
+ object gets created, or retrieved from the cache,
+ against a target :class:`.Session`, and is then invoked for results.
+
+ """
__slots__ = 'bq', 'session', '_params'
def __init__(self, bq, session):
@@ -149,6 +218,8 @@ class Result(object):
self._params = {}
def params(self, *args, **kw):
+ """Specify parameters to be replaced into the string SQL statement."""
+
if len(args) == 1:
kw.update(args[0])
elif len(args) > 0:
@@ -169,10 +240,9 @@ class Result(object):
if bq._spoiled:
return iter(self._as_query())
- if bq._cache_key not in bq._bakery:
- bq._bake(self.session)
-
- baked_context = bq._bakery[bq._cache_key]
+ baked_context = bq._bakery.get(bq._cache_key, None)
+ if baked_context is None:
+ baked_context = bq._bake(self.session)
context = copy.copy(baked_context)
context.session = self.session
@@ -187,6 +257,11 @@ class Result(object):
with_session(self.session)._execute_and_instances(context)
def first(self):
+ """Return the first row.
+
+ Equivalent to :meth:`.Query.first`.
+
+ """
bq = self.bq.with_criteria(lambda q: q.slice(0, 1))
ret = list(bq.for_session(self.session).params(self._params))
if len(ret) > 0:
@@ -197,6 +272,8 @@ class Result(object):
def one(self):
"""Return exactly one result or raise an exception.
+ Equivalent to :meth:`.Query.one`.
+
"""
ret = list(self)
@@ -210,9 +287,20 @@ class Result(object):
"Multiple rows were found for one()")
def all(self):
+ """Return all rows.
+
+ Equivalent to :meth:`.Query.all`.
+
+ """
return list(self)
def get(self, ident):
+ """Retrieve an object based on identity.
+
+ Equivalent to :meth:`.Query.get`.
+
+ """
+
query = self.bq.steps[0](self.session)
return query._get_impl(ident, self._load_on_ident)
@@ -268,6 +356,12 @@ class Result(object):
def bake_lazy_loaders():
+ """Enable the use of baked queries for all lazyloaders systemwide.
+
+ This operation should be safe for all lazy loaders, and will reduce
+ Python overhead for these operations.
+
+ """
strategies.LazyLoader._strategy_keys[:] = []
BakedLazyLoader._strategy_keys[:] = []
@@ -280,6 +374,11 @@ def bake_lazy_loaders():
def unbake_lazy_loaders():
+ """Disable the use of baked queries for all lazyloaders systemwide.
+
+ This operation reverts the changes produced by :func:`.bake_lazy_loaders`.
+
+ """
strategies.LazyLoader._strategy_keys[:] = []
BakedLazyLoader._strategy_keys[:] = []
@@ -298,14 +397,14 @@ class BakedLazyLoader(strategies.LazyLoader):
def _emit_lazyload(self, session, state, ident_key, passive):
q = BakedQuery(
- lambda session: session.query(self.mapper),
- bakery=self.mapper._compiled_cache)
+ self.mapper._compiled_cache,
+ lambda session: session.query(self.mapper))
q.add_criteria(
lambda q: q._adapt_all_clauses()._with_invoke_all_eagers(False),
self.parent_property)
if not self.parent_property.bake_queries:
- q.spoil()
+ q.spoil(full=True)
if self.parent_property.secondary is not None:
q.add_criteria(
@@ -396,3 +495,5 @@ def baked_lazyload_all(*keys):
baked_lazyload = baked_lazyload._unbound_fn
baked_lazyload_all = baked_lazyload_all._unbound_all_fn
+
+bakery = BakedQuery.bakery