Skip to content

Commit 6ba7e52

Browse files
authored
fix: all query types should use cache if available (#454)
* fix: all query types should use cache if available We were returning cache values in some cases and not others, generating some confusion Refs #441
1 parent 2d81ae5 commit 6ba7e52

4 files changed

Lines changed: 118 additions & 8 deletions

File tree

packages/google-cloud-ndb/google/cloud/ndb/_datastore_query.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from google.cloud.datastore_v1.proto import query_pb2
2525
from google.cloud.datastore import helpers
2626

27+
from google.cloud.ndb import context as context_module
2728
from google.cloud.ndb import _datastore_api
2829
from google.cloud.ndb import exceptions
2930
from google.cloud.ndb import key as key_module
@@ -52,6 +53,8 @@
5253
">=": query_pb2.PropertyFilter.GREATER_THAN_OR_EQUAL,
5354
}
5455

56+
_KEY_NOT_IN_CACHE = object()
57+
5558

5659
def make_filter(name, op, value):
5760
"""Make a property filter protocol buffer.
@@ -698,7 +701,7 @@ def _compare(self, other):
698701
return 0
699702

700703
def entity(self):
701-
"""Get an entity for an entity result.
704+
"""Get an entity for an entity result. Use the cache if available.
702705
703706
Args:
704707
projection (Optional[Sequence[str]]): Sequence of property names to
@@ -709,7 +712,21 @@ def entity(self):
709712
"""
710713

711714
if self.result_type == RESULT_TYPE_FULL:
712-
entity = model._entity_from_protobuf(self.result_pb.entity)
715+
# First check the cache.
716+
context = context_module.get_context()
717+
key_pb = self.result_pb.entity.key
718+
ds_key = helpers.key_from_protobuf(key_pb)
719+
key = key_module.Key._from_ds_key(ds_key)
720+
entity = _KEY_NOT_IN_CACHE
721+
use_cache = context._use_cache(key)
722+
if use_cache:
723+
try:
724+
entity = context.cache.get_and_validate(key)
725+
except KeyError:
726+
pass
727+
if entity is _KEY_NOT_IN_CACHE:
728+
# entity not in cache, create one.
729+
entity = model._entity_from_protobuf(self.result_pb.entity)
713730
return entity
714731

715732
elif self.result_type == RESULT_TYPE_PROJECTION:

packages/google-cloud-ndb/google/cloud/ndb/context.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,9 +279,9 @@ def _clear_global_cache(self):
279279
if keys:
280280
yield [_cache.global_delete(key) for key in keys]
281281

282-
def _use_cache(self, key, options):
282+
def _use_cache(self, key, options=None):
283283
"""Return whether to use the context cache for this key."""
284-
flag = options.use_cache
284+
flag = options.use_cache if options else None
285285
if flag is None:
286286
flag = self.cache_policy(key)
287287
if flag is None:

packages/google-cloud-ndb/tests/system/test_crud.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1429,3 +1429,47 @@ class SomeKind(ndb.Model):
14291429

14301430
retrieved = key.get()
14311431
assert retrieved.foo == datetime.datetime(2020, 8, 8, 1, 2, 3)
1432+
1433+
1434+
def test_cache_returns_entity_if_available(dispose_of, client_context):
1435+
"""Regression test for #441
1436+
1437+
https://github.com/googleapis/python-ndb/issues/441
1438+
"""
1439+
1440+
class SomeKind(ndb.Model):
1441+
foo = ndb.IntegerProperty()
1442+
bar = ndb.StringProperty()
1443+
1444+
client_context.set_cache_policy(None) # Use default
1445+
1446+
somekind = SomeKind(foo=1)
1447+
key = somekind.put()
1448+
dispose_of(key._key)
1449+
1450+
query = ndb.Query(kind="SomeKind")
1451+
ourkind = query.get()
1452+
ourkind.bar = "confusing"
1453+
1454+
assert somekind.bar == "confusing"
1455+
1456+
1457+
def test_cache_off_new_entity_created(dispose_of, client_context):
1458+
"""Regression test for #441
1459+
1460+
https://github.com/googleapis/python-ndb/issues/441
1461+
"""
1462+
1463+
class SomeKind(ndb.Model):
1464+
foo = ndb.IntegerProperty()
1465+
bar = ndb.StringProperty()
1466+
1467+
somekind = SomeKind(foo=1)
1468+
key = somekind.put()
1469+
dispose_of(key._key)
1470+
1471+
query = ndb.Query(kind="SomeKind")
1472+
ourkind = query.get()
1473+
ourkind.bar = "confusing"
1474+
1475+
assert somekind.bar is None

packages/google-cloud-ndb/tests/unit/test__datastore_query.py

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from google.cloud.datastore_v1.proto import query_pb2
2727

2828
from google.cloud.ndb import _datastore_query
29+
from google.cloud.ndb import context as context_module
2930
from google.cloud.ndb import exceptions
3031
from google.cloud.ndb import key as key_module
3132
from google.cloud.ndb import model
@@ -1052,16 +1053,64 @@ def test_entity_unsupported_result_type(model):
10521053
result.entity()
10531054

10541055
@staticmethod
1056+
@pytest.mark.usefixtures("in_context")
10551057
@mock.patch("google.cloud.ndb._datastore_query.model")
10561058
def test_entity_full_entity(model):
1057-
model._entity_from_protobuf.return_value = "bar"
1059+
key_pb = entity_pb2.Key(
1060+
partition_id=entity_pb2.PartitionId(project_id="testing"),
1061+
path=[entity_pb2.Key.PathElement(kind="ThisKind", id=42)],
1062+
)
1063+
entity = mock.Mock(key=key_pb)
1064+
model._entity_from_protobuf.return_value = entity
10581065
result = _datastore_query._Result(
10591066
_datastore_query.RESULT_TYPE_FULL,
1060-
mock.Mock(entity="foo", cursor=b"123", spec=("entity", "cursor")),
1067+
mock.Mock(entity=entity, cursor=b"123", spec=("entity", "cursor")),
10611068
)
10621069

1063-
assert result.entity() == "bar"
1064-
model._entity_from_protobuf.assert_called_once_with("foo")
1070+
assert result.entity() is entity
1071+
model._entity_from_protobuf.assert_called_once_with(entity)
1072+
1073+
@staticmethod
1074+
@pytest.mark.usefixtures("in_context")
1075+
@mock.patch("google.cloud.ndb._datastore_query.model")
1076+
def test_entity_full_entity_cached(model):
1077+
key = key_module.Key("ThisKind", 42)
1078+
key_pb = entity_pb2.Key(
1079+
partition_id=entity_pb2.PartitionId(project_id="testing"),
1080+
path=[entity_pb2.Key.PathElement(kind="ThisKind", id=42)],
1081+
)
1082+
entity = mock.Mock(key=key_pb)
1083+
cached_entity = mock.Mock(key=key_pb, _key=key)
1084+
context = context_module.get_context()
1085+
context.cache.data[key] = cached_entity
1086+
model._entity_from_protobuf.return_value = entity
1087+
result = _datastore_query._Result(
1088+
_datastore_query.RESULT_TYPE_FULL,
1089+
mock.Mock(entity=entity, cursor=b"123", spec=("entity", "cursor")),
1090+
)
1091+
1092+
assert result.entity() is not entity
1093+
assert result.entity() is cached_entity
1094+
1095+
@staticmethod
1096+
@pytest.mark.usefixtures("in_context")
1097+
@mock.patch("google.cloud.ndb._datastore_query.model")
1098+
def test_entity_full_entity_no_cache(model):
1099+
context = context_module.get_context()
1100+
with context.new(cache_policy=False).use():
1101+
key_pb = entity_pb2.Key(
1102+
partition_id=entity_pb2.PartitionId(project_id="testing"),
1103+
path=[entity_pb2.Key.PathElement(kind="ThisKind", id=42)],
1104+
)
1105+
entity = mock.Mock(key=key_pb)
1106+
model._entity_from_protobuf.return_value = entity
1107+
result = _datastore_query._Result(
1108+
_datastore_query.RESULT_TYPE_FULL,
1109+
mock.Mock(
1110+
entity=entity, cursor=b"123", spec=("entity", "cursor")
1111+
),
1112+
)
1113+
assert result.entity() is entity
10651114

10661115
@staticmethod
10671116
@pytest.mark.usefixtures("in_context")

0 commit comments

Comments
 (0)