]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Add new configuration, inspection for baked queries
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 17 May 2017 17:05:04 +0000 (13:05 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 17 May 2017 21:08:23 +0000 (17:08 -0400)
Added new flag :paramref:`.Session.enable_baked_queries` to the
:class:`.Session` to allow baked queries to be disabled
session-wide, reducing memory use.   Also added new :class:`.Bakery`
wrapper so that the bakery returned by :paramref:`.BakedQuery.bakery`
can be inspected.

Change-Id: I5657af7a99d2b24c89d6aee1343f432728e3f807

doc/build/changelog/changelog_12.rst
doc/build/orm/extensions/baked.rst
lib/sqlalchemy/ext/baked.py
lib/sqlalchemy/orm/session.py
test/ext/test_baked.py

index 3d0b7fa0884be2dc5389b4f0b381b7e31fb21565..d8efd74a9a4ad0910a8b210385bc7382a24996ad 100644 (file)
 .. changelog::
     :version: 1.2.0b1
 
+    .. change:: baked_opts
+        :tags: feature, ext
+
+        Added new flag :paramref:`.Session.enable_baked_queries` to the
+        :class:`.Session` to allow baked queries to be disabled
+        session-wide, reducing memory use.   Also added new :class:`.Bakery`
+        wrapper so that the bakery returned by :paramref:`.BakedQuery.bakery`
+        can be inspected.
+
     .. change:: 3988
         :tags: bug, orm
         :tickets: 3988
index 0dceedc4a1ee2716e11adaad263c878951a7aaed..593293b4606c639254d4efae80d09184bff7b348 100644 (file)
@@ -332,6 +332,27 @@ to arrive at the current "baked" approach.   Starting from the
 management,  removal of all redundant Python execution, and queries built up
 with conditionals needed to be addressed, leading to the final approach.
 
+Disabling Baked Queries Session-wide
+------------------------------------
+
+The flag :paramref:`.Session.enable_baked_queries` may be set to False,
+causing all baked queries to not use the cache when used against that
+:class:`.Session`::
+
+    session = Session(engine, enable_baked_queries=False)
+
+Like all session flags, it is also accepted by factory objects like
+:class:`.sessionmaker` and methods like :meth:`.sessionmaker.configure`.
+
+The immediate rationale for this flag is to reduce memory use in the case
+that the query baking used by relationship loaders and other loaders
+is not desirable.   It also can be used in the case that an application
+which is seeing issues potentially due to cache key conflicts from user-defined
+baked queries or other baked query issues can turn the behavior off, in
+order to identify or eliminate baked queries as the cause of an issue.
+
+.. versionadded:: 1.2
+
 Lazy Loading Integration
 ------------------------
 
@@ -350,7 +371,11 @@ Opting out with the bake_queries flag
 
 The :func:`.relationship` construct includes a flag
 :paramref:`.relationship.bake_queries` which when set to False will cause
-that relationship to opt out of caching queries.
+that relationship to opt out of caching queries.  Additionally, the
+:paramref:`.Session.enable_baked_queries` setting can be used to disable
+all "baked query" use.   These flags can be useful to conserve memory,
+when memory conservation is more important than performance for a particular
+relationship or for the application overall.
 
 API Documentation
 -----------------
@@ -360,6 +385,9 @@ API Documentation
 .. autoclass:: BakedQuery
     :members:
 
+.. autoclass:: Bakery
+    :members:
+
 .. autoclass:: Result
     :members:
 
index 95b618f3f5dd0b6a9cb7e445bc74bf1dbc64d162..ba3c2aed045bbb2a7f3da3c7b4a3da3874102a90 100644 (file)
@@ -28,6 +28,27 @@ import logging
 log = logging.getLogger(__name__)
 
 
+class Bakery(object):
+    """Callable which returns a :class:`.BakedQuery`.
+
+    This object is returned by the class method
+    :meth:`.BakedQuery.bakery`.  It exists as an object
+    so that the "cache" can be easily inspected.
+
+    .. versionadded:: 1.2
+
+
+    """
+    __slots__ = 'cls', 'cache'
+
+    def __init__(self, cls_, cache):
+        self.cls = cls_
+        self.cache = cache
+
+    def __call__(self, initial_fn, *args):
+        return self.cls(self.cache, initial_fn, args)
+
+
 class BakedQuery(object):
     """A builder object for :class:`.query.Query` objects."""
 
@@ -42,14 +63,13 @@ class BakedQuery(object):
 
     @classmethod
     def bakery(cls, size=200, _size_alert=None):
-        """Construct a new bakery."""
+        """Construct a new bakery.
 
-        _bakery = util.LRUCache(size, size_alert=_size_alert)
+        :return: an instance of :class:`.Bakery`
 
-        def call(initial_fn, *args):
-            return cls(_bakery, initial_fn, args)
+        """
 
-        return call
+        return Bakery(cls, util.LRUCache(size, size_alert=_size_alert))
 
     def _clone(self):
         b1 = BakedQuery.__new__(BakedQuery)
@@ -265,7 +285,7 @@ class Result(object):
 
     def __iter__(self):
         bq = self.bq
-        if bq._spoiled:
+        if not self.session.enable_baked_queries or bq._spoiled:
             return iter(self._as_query())
 
         baked_context = bq._bakery.get(bq._cache_key, None)
index 3585166f49124fa42fafbf8c76a340a70f42292d..6186ac4f7c855d1926dfbacdff65380afc7a6c01 100644 (file)
@@ -588,6 +588,7 @@ class Session(_SessionClassMethods):
                  _enable_transaction_accounting=True,
                  autocommit=False, twophase=False,
                  weak_identity_map=True, binds=None, extension=None,
+                 enable_baked_queries=True,
                  info=None,
                  query_cls=query.Query):
         r"""Construct a new Session.
@@ -661,6 +662,21 @@ class Session(_SessionClassMethods):
            :class:`.sessionmaker` function, and is not sent directly to the
            constructor for ``Session``.
 
+        :param enable_baked_queries: defaults to ``True``.  A flag consumed
+           by the :mod:`sqlalchemy.ext.baked` extension to determine if
+           "baked queries" should be cached, as is the normal operation
+           of this extension.  When set to ``False``, all caching is disabled,
+           including baked queries defined by the calling application as
+           well as those used internally.  Setting this flag to ``False``
+           can significantly reduce memory use, however will also degrade
+           performance for those areas that make use of baked queries
+           (such as relationship loaders).   Additionally, baked query
+           logic in the calling application or potentially within the ORM
+           that may be malfunctioning due to cache key collisions or similar
+           can be flagged by observing if this flag resolves the issue.
+
+           .. versionadded:: 1.2
+
         :param _enable_transaction_accounting:  Defaults to ``True``.  A
            legacy-only flag which when ``False`` disables *all* 0.5-style
            object accounting on transaction boundaries, including auto-expiry
@@ -735,6 +751,7 @@ class Session(_SessionClassMethods):
         self.autoflush = autoflush
         self.autocommit = autocommit
         self.expire_on_commit = expire_on_commit
+        self.enable_baked_queries = enable_baked_queries
         self._enable_transaction_accounting = _enable_transaction_accounting
         self.twophase = twophase
         self._query_cls = query_cls
index 263a1bb6c367f3f1527ab5cf6d052f59e84215cd..d2fcfbab85c60487ebddbf7dd53aff5d98846e01 100644 (file)
@@ -447,6 +447,41 @@ class ResultTest(BakedTest):
                     exp
                 )
 
+    def test_disable_on_session(self):
+        User = self.classes.User
+
+        canary = mock.Mock()
+
+        def fn1(s):
+            canary.fn1()
+            return s.query(User.id, User.name).order_by(User.id)
+
+        def fn2(q):
+            canary.fn2()
+            return q.filter(User.id == bindparam('id'))
+
+        def fn3(q):
+            canary.fn3()
+            return q
+
+        for x in range(3):
+            bq = self.bakery(fn1)
+
+            bq += fn2
+
+            sess = Session(autocommit=True, enable_baked_queries=False)
+            eq_(
+                bq.add_criteria(fn3)(sess).params(id=7).all(),
+                [(7, 'jack')]
+            )
+
+        eq_(
+            canary.mock_calls,
+            [mock.call.fn1(), mock.call.fn2(), mock.call.fn3(),
+             mock.call.fn1(), mock.call.fn2(), mock.call.fn3(),
+             mock.call.fn1(), mock.call.fn2(), mock.call.fn3()]
+        )
+
     def test_spoiled_full_w_params(self):
         User = self.classes.User