-
Notifications
You must be signed in to change notification settings - Fork 13
feat: cache queries in filters #124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1365872
93d8f9e
652d843
004f694
d8d2bd8
10fd415
07e42ad
4737a2d
b6ce189
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| [bumpversion] | ||
| current_version = 0.25.3 | ||
| current_version = 0.26.0 | ||
|
|
||
| [bumpversion:file:pyproject.toml] | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,12 +7,16 @@ | |
|
|
||
| from collections import defaultdict | ||
| import inspect | ||
| import logging | ||
| import types | ||
|
|
||
| from django.conf import settings | ||
| from django.db import models | ||
| from django.db.models import ManyToOneRel | ||
| from django.core.exceptions import FieldDoesNotExist | ||
|
|
||
| logger = logging.getLogger() | ||
|
|
||
| NEVER = 0 | ||
| ALWAYS = 1 | ||
| IF_TRUTHY = 2 | ||
|
|
@@ -63,8 +67,14 @@ def _get_unexpanded_field_value(inst, field_name, field_type): | |
| return {display_key: getattr(obj, display_key)} | ||
|
|
||
|
|
||
| def _get_filtered_field_value( | ||
| inst, field_name, field_type, filter_def, expand_this, expand_children | ||
| def _get_filtered_field_value( # noqa: C901 | ||
| inst, | ||
| field_name, | ||
| field_type, | ||
| filter_def, | ||
| expand_this, | ||
| expand_children, | ||
| filter_cache, | ||
| ): | ||
| # get the value from inst | ||
| if field_type == NEVER: | ||
|
|
@@ -80,6 +90,25 @@ def _get_filtered_field_value( | |
| val = _get_unexpanded_field_value(inst, field_name, field_type) | ||
| else: | ||
| try: | ||
| if isinstance(inst, (models.Model)): | ||
| try: | ||
| field_meta = inst._meta.get_field(field_name) | ||
| if field_meta.is_relation: | ||
| val_pk = getattr(inst, field_meta.attname) | ||
| val_cls = field_meta.related_model | ||
| val_expand_children = expand_children.get(field_name, {}) | ||
| cache_key = _make_filter_cache_key( | ||
| val_expand_children, val_cls, val_pk | ||
| ) | ||
| if cache_key in filter_cache: | ||
| logger.debug( | ||
| "ev=filter_cache, status=hit, key=%s", cache_key | ||
| ) | ||
| return filter_cache[cache_key] | ||
| except FieldDoesNotExist: | ||
| # this happens when you reference the special field "pk" in filters | ||
| pass | ||
|
|
||
| val = getattr(inst, field_name) | ||
| except (AttributeError, FieldDoesNotExist) as e: # noqa | ||
| return None | ||
|
|
@@ -93,7 +122,11 @@ def _get_filtered_field_value( | |
| val, (list, tuple, models.Model, models.query.QuerySet) | ||
| ): | ||
| val = _apply_filters_to_object( | ||
| val, filter_def, expand_children=expand_children, klass=val.__class__ | ||
| val, | ||
| filter_def, | ||
| expand_children=expand_children, | ||
| klass=val.__class__, | ||
| filter_cache=filter_cache, | ||
| ) | ||
|
|
||
| if ( | ||
|
|
@@ -107,22 +140,53 @@ def _get_filtered_field_value( | |
| return None | ||
|
|
||
|
|
||
| def _make_filter_cache_key(expand_children, klass, pk): | ||
| return (str(expand_children), klass, str(pk)) | ||
|
|
||
|
|
||
| # TODO: make this method less complex and remove the `noqa` | ||
| def _apply_filters_to_object( # noqa: C901 | ||
| inst, filter_def, expand_children=None, klass=None | ||
| inst, | ||
| filter_def, | ||
| expand_children=None, | ||
| klass=None, | ||
| filter_cache=None, | ||
| ): | ||
| is_cacheable = False | ||
| caching_enabled = getattr(settings, "DDA_FILTER_MODEL_CACHING_ENABLED", False) | ||
| if ( | ||
| caching_enabled | ||
| and isinstance(inst, (models.Model,)) | ||
| and filter_cache is not None | ||
| ): | ||
| is_cacheable = True | ||
| pk = getattr(inst, "pk") | ||
| cache_key = _make_filter_cache_key(expand_children, klass, pk) | ||
| if cache_key in filter_cache: | ||
| logger.debug("ev=filter_cache, status=hit, key=%s", cache_key) | ||
| return filter_cache[cache_key] | ||
| else: | ||
| logger.debug("ev=filter_cache, status=miss, key=%s", cache_key) | ||
| if isinstance(inst, (list, tuple, models.query.QuerySet)): | ||
| # if it's a tuple or list, iterate over the collection and call _apply_filters_to_object on each item | ||
| return [ | ||
| _apply_filters_to_object( | ||
| item, filter_def, expand_children=expand_children, klass=item.__class__ | ||
| item, | ||
| filter_def, | ||
| expand_children=expand_children, | ||
| klass=item.__class__, | ||
| filter_cache=filter_cache, | ||
| ) | ||
| for item in inst | ||
| ] | ||
| elif isinstance(inst, (dict,)): | ||
| return { | ||
| k: _apply_filters_to_object( | ||
| v, filter_def, expand_children=expand_children, klass=v.__class__ | ||
| v, | ||
| filter_def, | ||
| expand_children=expand_children, | ||
| klass=v.__class__, | ||
| filter_cache=filter_cache, | ||
| ) | ||
| for k, v in inst.items() | ||
| } | ||
|
|
@@ -134,16 +198,28 @@ def _apply_filters_to_object( # noqa: C901 | |
| for base_class in inspect.getmro(klass): | ||
| if base_class in filter_def: | ||
| result = _apply_filters_to_object( | ||
| inst, filter_def, expand_children=expand_children, klass=base_class | ||
| inst, | ||
| filter_def, | ||
| expand_children=expand_children, | ||
| klass=base_class, | ||
| filter_cache=filter_cache, | ||
| ) | ||
| break | ||
|
|
||
| if is_cacheable: | ||
| filter_cache[cache_key] = result | ||
|
|
||
| return result | ||
| else: | ||
| # first, recursively populate from any ancestor classes in the inheritance hierarchy | ||
| result = defaultdict(list) | ||
| for base in klass.__bases__: | ||
| filtered_ancestor = _apply_filters_to_object( | ||
| inst, filter_def, expand_children=expand_children, klass=base | ||
| inst, | ||
| filter_def, | ||
| expand_children=expand_children, | ||
| klass=base, | ||
| filter_cache=filter_cache, | ||
| ) | ||
| if filtered_ancestor: | ||
| result.update(filtered_ancestor) | ||
|
|
@@ -166,6 +242,7 @@ def _apply_filters_to_object( # noqa: C901 | |
| filter_def, | ||
| expand_this=field_name in expand_children, | ||
| expand_children=expand_children.get(field_name, {}), | ||
| filter_cache=filter_cache, | ||
| ) | ||
|
|
||
| if value is not None and value != DEFAULT_UNEXPANDED_VALUE: | ||
|
|
@@ -176,6 +253,9 @@ def _apply_filters_to_object( # noqa: C901 | |
| if expandables: | ||
| result[EXPANDABLE_FIELD_KEY] += expandables | ||
|
|
||
| if is_cacheable: | ||
| filter_cache[cache_key] = result | ||
|
|
||
| return result | ||
|
|
||
|
|
||
|
|
@@ -195,5 +275,9 @@ def apply_filters_to_object(inst, filter_def, expand_header=""): | |
| else: | ||
| expand_dict = {} | ||
| return _apply_filters_to_object( | ||
| inst, filter_def, expand_children=expand_dict, klass=inst.__class__ | ||
| inst, | ||
| filter_def, | ||
| expand_children=expand_dict, | ||
| klass=inst.__class__, | ||
| filter_cache={}, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How concerned should we be about the size of this object that we're passing? It could be massive, right?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe worthwhile doing something like this to track?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @smholloway it's pass-by-reference, so no concern about size when passing it in function calls. I don't even really think we need to track it; the total
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright. Makes sense to me 👍 |
||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
klasswould end up being the string repr of the instance's class, right? Wouldf"{klass.__module__}.{klass.__name__}"be slightly preferable?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
klassis the actualtypeobject, not a str.expand_childrenhas to be stringified becausedictsare not hashable.pkhas to be stringified because otherwise we get a mix ofuuidandstrthat don't match like they should.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I'm dumb. I was thinking it was being serialized for some reason.