From: Mike Bayer Date: Wed, 16 May 2018 20:31:40 +0000 (-0400) Subject: Update dogpile.cache example to be compatible with baked query. X-Git-Tag: rel_1_3_0b1~181^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=2c7f2954f793cbb4d991a870c1f725fc98e2e83f;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Update dogpile.cache example to be compatible with baked query. 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 --- diff --git a/doc/build/changelog/unreleased_12/4258.rst b/doc/build/changelog/unreleased_12/4258.rst new file mode 100644 index 0000000000..62a095375a --- /dev/null +++ b/doc/build/changelog/unreleased_12/4258.rst @@ -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`. diff --git a/examples/dogpile_caching/caching_query.py b/examples/dogpile_caching/caching_query.py index 9ac0d431ab..ed5a01f573 100644 --- a/examples/dogpile_caching/caching_query.py +++ b/examples/dogpile_caching/caching_query.py @@ -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 diff --git a/test/ext/test_baked.py b/test/ext/test_baked.py index 6344819e16..37065ec713 100644 --- a/test/ext/test_baked.py +++ b/test/ext/test_baked.py @@ -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)]})