summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/sql/traversals.py
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2020-03-09 17:12:35 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2020-04-01 16:12:23 -0400
commita9b62055bfa61c11e9fe0b2984437e2c3e32bf0e (patch)
tree366027c7069edd56d49e9d540ae6a14fbe9e16fe /lib/sqlalchemy/sql/traversals.py
parente6250123a30e457068878394e49b7ca07ca4d3b0 (diff)
downloadsqlalchemy-a9b62055bfa61c11e9fe0b2984437e2c3e32bf0e.tar.gz
Try to measure new style caching in the ORM, take two
Supercedes: If78fbb557c6f2cae637799c3fec2cbc5ac248aaf Trying to see if by making the cache key memoized, we still can have the older "identity" form of caching which is the cheapest of all, at the same time as the newer "cache key each time" version that is not nearly as cheap; but still much cheaper than no caching at all. Also needed is a per-execution update of _keymap when we invoke from a cached select, so that Column objects that are anonymous or otherwise adapted will match up. this is analogous to the adaption of bound parameters from the cache key. Adds test coverage for the keymap / construct_params() changes related to caching. Also hones performance to a large extent for statement construction and cache key generation. Also includes a new memoized attribute approach that vastly simplifies the previous approach of "group_expirable_memoized_property" and finally integrates cleanly with _clone(), _generate(), etc. no more hardcoding of attributes is needed, as well as that most _reset_memoization() calls are no longer needed as the reset is inherent in a _generate() call; this also has dramatic performance improvements. Change-Id: I95c560ffcbfa30b26644999412fb6a385125f663
Diffstat (limited to 'lib/sqlalchemy/sql/traversals.py')
-rw-r--r--lib/sqlalchemy/sql/traversals.py60
1 files changed, 57 insertions, 3 deletions
diff --git a/lib/sqlalchemy/sql/traversals.py b/lib/sqlalchemy/sql/traversals.py
index 1fcc2d023..9ac6cda97 100644
--- a/lib/sqlalchemy/sql/traversals.py
+++ b/lib/sqlalchemy/sql/traversals.py
@@ -7,6 +7,7 @@ from .visitors import ExtendedInternalTraversal
from .visitors import InternalTraversal
from .. import util
from ..inspection import inspect
+from ..util import HasMemoized
SKIP_TRAVERSE = util.symbol("skip_traverse")
COMPARE_FAILED = False
@@ -26,7 +27,7 @@ def compare(obj1, obj2, **kw):
return strategy.compare(obj1, obj2, **kw)
-class HasCacheKey(object):
+class HasCacheKey(HasMemoized):
_cache_key_traversal = NO_CACHE
__slots__ = ()
@@ -105,6 +106,14 @@ class HasCacheKey(object):
attrname,
obj._gen_cache_key(anon_map, bindparams),
)
+ elif meth is InternalTraversal.dp_annotations_key:
+ # obj is here is the _annotations dict. however,
+ # we want to use the memoized cache key version of it.
+ # for Columns, this should be long lived. For select()
+ # statements, not so much, but they usually won't have
+ # annotations.
+ if obj:
+ result += self._annotations_cache_key
elif meth is InternalTraversal.dp_clauseelement_list:
if obj:
result += (
@@ -130,6 +139,7 @@ class HasCacheKey(object):
return result
+ @HasMemoized.memoized_instancemethod
def _generate_cache_key(self):
"""return a cache key.
@@ -161,6 +171,7 @@ class HasCacheKey(object):
will return None, indicating no cache key is available.
"""
+
bindparams = []
_anon_map = anon_map()
@@ -178,6 +189,36 @@ class CacheKey(namedtuple("CacheKey", ["key", "bindparams"])):
def __eq__(self, other):
return self.key == other.key
+ def __str__(self):
+ stack = [self.key]
+
+ output = []
+ sentinel = object()
+ indent = -1
+ while stack:
+ elem = stack.pop(0)
+ if elem is sentinel:
+ output.append((" " * (indent * 2)) + "),")
+ indent -= 1
+ elif isinstance(elem, tuple):
+ if not elem:
+ output.append((" " * ((indent + 1) * 2)) + "()")
+ else:
+ indent += 1
+ stack = list(elem) + [sentinel] + stack
+ output.append((" " * (indent * 2)) + "(")
+ else:
+ if isinstance(elem, HasCacheKey):
+ repr_ = "<%s object at %s>" % (
+ type(elem).__name__,
+ hex(id(elem)),
+ )
+ else:
+ repr_ = repr(elem)
+ output.append((" " * (indent * 2)) + " " + repr_ + ", ")
+
+ return "CacheKey(key=%s)" % ("\n".join(output),)
+
def _clone(element, **kw):
return element._clone()
@@ -189,6 +230,8 @@ class _CacheKey(ExtendedInternalTraversal):
visit_has_cache_key = visit_clauseelement = CALL_GEN_CACHE_KEY
visit_clauseelement_list = InternalTraversal.dp_clauseelement_list
+ visit_annotations_key = InternalTraversal.dp_annotations_key
+
visit_string = (
visit_boolean
) = visit_operator = visit_plain_obj = CACHE_IN_PLACE
@@ -690,8 +733,8 @@ class TraversalComparatorStrategy(InternalTraversal, util.MemoizedSlots):
fillvalue=(None, None),
):
if not compare_annotations and (
- (left_attrname == "_annotations_cache_key")
- or (right_attrname == "_annotations_cache_key")
+ (left_attrname == "_annotations")
+ or (right_attrname == "_annotations")
):
continue
@@ -827,6 +870,17 @@ class TraversalComparatorStrategy(InternalTraversal, util.MemoizedSlots):
):
return left == right
+ def visit_annotations_key(
+ self, left_parent, left, right_parent, right, **kw
+ ):
+ if left and right:
+ return (
+ left_parent._annotations_cache_key
+ == right_parent._annotations_cache_key
+ )
+ else:
+ return left == right
+
def visit_plain_obj(self, left_parent, left, right_parent, right, **kw):
return left == right