]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Update dogpile.cache example to be compatible with baked query.
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 16 May 2018 20:31:40 +0000 (16:31 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 17 May 2018 16:43:44 +0000 (12:43 -0400)
Updated the dogpile.caching example to include new structures that
accommodate for the "baked" query system, which is used by default within
lazy loaders and some eager relationship loaders. The dogpile.caching
"relationship_caching" and "advanced" examples were also broken due to
:ticket:`4256`.  The issue here is also worked-around by the fix in
:ticket:`4128`.

Note that this recipe requires
I3f86fcb12a6a9a89aa308b335e75c25969bcc30e in order for the
"advanced" example to work.

Change-Id: I9d35417f1d6c1906555583b8225d3da7f81736f7
Fixes: #4258
doc/build/changelog/unreleased_12/4258.rst [new file with mode: 0644]
examples/dogpile_caching/caching_query.py
test/ext/test_baked.py

diff --git a/doc/build/changelog/unreleased_12/4258.rst b/doc/build/changelog/unreleased_12/4258.rst
new file mode 100644 (file)
index 0000000..62a0953
--- /dev/null
@@ -0,0 +1,10 @@
+.. change::
+    :tags: bug, orm
+    :tickets: 4258
+
+    Updated the dogpile.caching example to include new structures that
+    accommodate for the "baked" query system, which is used by default within
+    lazy loaders and some eager relationship loaders. The dogpile.caching
+    "relationship_caching" and "advanced" examples were also broken due to
+    :ticket:`4256`.  The issue here is also worked-around by the fix in
+    :ticket:`4128`.
index 9ac0d431ab5370b12bcbecd8550456106d39c751..ed5a01f573b1d8608b5391f56f520e634dbfc766 100644 (file)
@@ -59,10 +59,37 @@ class CachingQuery(Query):
            in the cache are not the same ones in the current Session.
 
         """
+        super_ = super(CachingQuery, self)
+
         if hasattr(self, '_cache_region'):
-            return self.get_value(createfunc=lambda: list(Query.__iter__(self)))
+            return self.get_value(createfunc=lambda: list(super_.__iter__()))
+        else:
+            return super_.__iter__()
+
+    def _execute_and_instances(self, context):
+        """override _execute_and_instances to pull results from dogpile
+            if the query is invoked directly from an external context.
+
+           This method is necessary in order to maintain compatibility
+           with the "baked query" system now used by default in some
+           relationship loader scenarios.   Note also the
+           RelationshipCache._generate_cache_key method which enables
+           the baked query to be used within lazy loads.
+
+           .. versionadded:: 1.2.7
+        """
+        super_ = super(CachingQuery, self)
+
+        if context.query is not self and hasattr(self, '_cache_region'):
+            # special logic called when the Query._execute_and_instances()
+            # method is called directly from the baked query
+            return self.get_value(
+                createfunc=lambda: list(
+                    super_._execute_and_instances(context)
+                )
+            )
         else:
-            return Query.__iter__(self)
+            return super_._execute_and_instances(context)
 
     def _get_cache_plus_key(self):
         """Return a cache region plus key."""
@@ -227,4 +254,16 @@ class RelationshipCache(MapperOption):
         self._relationship_options.update(option._relationship_options)
         return self
 
+    def _generate_cache_key(self, path):
+        """Indicate to the lazy-loader strategy that a "baked" query
+        may be used by returning ``None``.
+
+        If this method is omitted, the default implementation of
+        :class:`.MapperOption._generate_cache_key` takes place, which
+        returns ``False`` to disable the "baked" query from being used.
+
+        .. versionadded:: 1.2.7
+
+        """
+        return None
 
index 6344819e160ebaaf1f71aa99e148e10bc16f6650..37065ec713b35f502f756e54126c99ee68a1319a 100644 (file)
@@ -1169,3 +1169,145 @@ class LazyLoaderTest(testing.AssertsCompiledSQL, BakedTest):
     # 2. o2m lazyload where m2o backrefs have an eager load, test
     # that eager load is canceled out
     # 3. uselist = False, uselist=False assertion
+
+# assert that the integration style illustrated in the dogpile.cache
+# example works w/ baked
+class CustomIntegrationTest(testing.AssertsCompiledSQL, BakedTest):
+    run_setup_mappers = 'each'
+
+    def _o2m_fixture(self, lazy="select", **kw):
+        User = self.classes.User
+        Address = self.classes.Address
+
+        mapper(User, self.tables.users, properties={
+            'addresses': relationship(
+                Address, order_by=self.tables.addresses.c.id,
+                lazy=lazy, **kw)
+        })
+        mapper(Address, self.tables.addresses)
+        return User, Address
+
+    def _query_fixture(self):
+        from sqlalchemy.orm.query import Query, _generative
+
+        class CachingQuery(Query):
+            cache = {}
+
+            @_generative()
+            def set_cache_key(self, key):
+                self._cache_key = key
+
+            def __iter__(self):
+                super_ = super(CachingQuery, self)
+
+                if hasattr(self, '_cache_key'):
+                    return self.get_value(
+                        createfunc=lambda: list(super_.__iter__()))
+                else:
+                    return super_.__iter__()
+
+            def _execute_and_instances(self, context):
+                super_ = super(CachingQuery, self)
+
+                if context.query is not self and hasattr(self, '_cache_key'):
+                    return self.get_value(
+                        createfunc=lambda: list(
+                            super_._execute_and_instances(context)
+                        )
+                    )
+                else:
+                    return super_._execute_and_instances(context)
+
+            def get_value(self, createfunc):
+                if self._cache_key in self.cache:
+                    return iter(self.cache[self._cache_key])
+                else:
+                    self.cache[self._cache_key] = retval = createfunc()
+                    return iter(retval)
+
+        return Session(query_cls=CachingQuery)
+
+    def _option_fixture(self):
+        from sqlalchemy.orm.interfaces import MapperOption
+
+        class RelationshipCache(MapperOption):
+
+            propagate_to_loaders = True
+
+            def process_query_conditionally(self, query):
+                if query._current_path:
+                    query._cache_key = "user7_addresses"
+
+            def _generate_cache_key(self, path):
+                return None
+
+        return RelationshipCache()
+
+    def test_non_baked(self):
+        User, Address = self._o2m_fixture()
+
+        sess = self._query_fixture()
+        q = sess._query_cls
+        eq_(q.cache, {})
+
+        q = sess.query(User).filter(User.id == 7).set_cache_key("user7")
+
+        eq_(
+            q.all(),
+            [User(id=7, addresses=[Address(id=1)])]
+        )
+
+        eq_(q.cache, {"user7": [User(id=7, addresses=[Address(id=1)])]})
+
+        eq_(
+            q.all(),
+            [User(id=7, addresses=[Address(id=1)])]
+        )
+
+    def test_use_w_baked(self):
+        User, Address = self._o2m_fixture()
+
+        sess = self._query_fixture()
+        q = sess._query_cls
+        eq_(q.cache, {})
+
+        base_bq = self.bakery(
+            lambda s: s.query(User))
+        base_bq += lambda q: q.filter(User.id == 7)
+        base_bq += lambda q: q.set_cache_key("user7")
+
+        eq_(
+            base_bq(sess).all(),
+            [User(id=7, addresses=[Address(id=1)])]
+        )
+
+        eq_(q.cache, {"user7": [User(id=7, addresses=[Address(id=1)])]})
+
+        eq_(
+            base_bq(sess).all(),
+            [User(id=7, addresses=[Address(id=1)])]
+        )
+
+    def test_plain_w_baked_lazyload(self):
+        User, Address = self._o2m_fixture()
+        opt = self._option_fixture()
+
+        sess = self._query_fixture()
+        q = sess._query_cls
+        eq_(q.cache, {})
+
+        q = sess.query(User).filter(User.id == 7).options(opt)
+
+        u = q.first()
+        eq_(u.addresses, [Address(id=1)])
+
+        eq_(q.cache, {"user7_addresses": [Address(id=1)]})
+
+        sess.close()
+
+        # ensure caching logic works after query has been baked
+        q.cache.clear()
+
+        u = q.first()
+        eq_(u.addresses, [Address(id=1)])
+        eq_(q.cache, {"user7_addresses": [Address(id=1)]})