-
Notifications
You must be signed in to change notification settings - Fork 12
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
feat: cache functions and fake relations in filters #127
Changes from 7 commits
cdcc923
e36fa6f
cb999ae
9c4c3ac
308f982
686065c
4011413
0855863
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.27.0 | ||
current_version = 0.28.0 | ||
|
||
[bumpversion:file:pyproject.toml] | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,11 +11,12 @@ | |
import types | ||
|
||
from django.conf import settings | ||
from django.core.exceptions import FieldDoesNotExist | ||
from django.db import models | ||
from django.db.models import ManyToOneRel | ||
from django.core.exceptions import FieldDoesNotExist | ||
from django.db.models.fields.reverse_related import ForeignObjectRel | ||
|
||
logger = logging.getLogger() | ||
logger = logging.getLogger(__name__) | ||
|
||
NEVER = 0 | ||
ALWAYS = 1 | ||
|
@@ -38,7 +39,7 @@ def expandable(model_class=None, display_key=None, inst_field_name=None): | |
if model_class and display_key: | ||
try: | ||
model_class._meta.get_field(display_key) | ||
except models.FieldDoesNotExist as e: # noqa | ||
except FieldDoesNotExist: | ||
raise ValueError(f"{display_key} is not a field on {model_class.__name__}") | ||
return _ExpandableForeignKey(display_key, model_class, inst_field_name) | ||
|
||
|
@@ -67,6 +68,36 @@ def _get_unexpanded_field_value(inst, field_name, field_type): | |
return {display_key: getattr(obj, display_key)} | ||
|
||
|
||
def _is_relation(field): | ||
return field.is_relation or getattr(field, "is_fake_relation", False) | ||
|
||
|
||
def _is_reverse_relation(field): | ||
return isinstance(field, ForeignObjectRel) | ||
|
||
|
||
def _cache_related_instance(inst, field_name, model_cache): | ||
try: | ||
field_meta = inst._meta.get_field(field_name) | ||
except (AttributeError, FieldDoesNotExist): | ||
return | ||
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. I'm curious why we're returning None here, shouldn't we bubble up the error? The user is trying to cache a field that doesn't exist 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. The problem is that FILTERS = {
'my_increased_counter': lambda inst: inst.counter.count + 1
} Here 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. Right, but shouldn't we alert the user that the field doesn't exist somehow? 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. Code won't break if the field doesn't exists. It just won't get cached. Or do mean alerting the user that using 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. I think I understand what you're saying now. This PR looks good, approved! |
||
if not _is_relation(field_meta) or _is_reverse_relation(field_meta): | ||
return # no `attname` in reverse relations | ||
val_pk = getattr(inst, field_meta.attname) | ||
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. can't we just wrap this in a try catch block and log the error or reraise the error? 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. that's a good point. I had run over the code again to be sure and the problem is that some fields have 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. ah ok. |
||
val_cls = field_meta.related_model | ||
cache_key = _make_model_cache_key(val_cls, val_pk) | ||
if cache_key in model_cache: | ||
logger.debug("ev=model_cache, status=hit, key=%s", cache_key) | ||
related_instance = model_cache[cache_key] | ||
else: | ||
logger.debug("ev=model_cache, status=miss, key=%s", cache_key) | ||
related_instance = getattr(inst, field_name) | ||
if not isinstance(related_instance, models.Model): | ||
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. Seems very generic, can't we use the instance type of 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. good catch. yeah, I like it 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. added it |
||
return | ||
model_cache[cache_key] = related_instance | ||
setattr(inst, field_name, related_instance) | ||
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. idea here is to pre-populate the related instance so if the function decides to access it or use it (normally it does) we don't do another query to the db. In example: FILTERS = {
'counter': lambda inst: inst.counter.count + 1
} When that function filter is called |
||
|
||
|
||
def _get_filtered_field_value( # noqa: C901 | ||
inst, | ||
field_name, | ||
|
@@ -75,12 +106,15 @@ def _get_filtered_field_value( # noqa: C901 | |
expand_this, | ||
expand_children, | ||
filter_cache, | ||
model_cache, | ||
): | ||
# get the value from inst | ||
if field_type == NEVER: | ||
return None | ||
|
||
if isinstance(field_type, types.FunctionType): | ||
if is_caching_enabled(): | ||
_cache_related_instance(inst, field_name, model_cache) | ||
val = field_type(inst) | ||
elif isinstance(field_type, _ExpandableForeignKey): | ||
if expand_this: | ||
|
@@ -93,7 +127,7 @@ def _get_filtered_field_value( # noqa: C901 | |
if isinstance(inst, (models.Model)): | ||
try: | ||
field_meta = inst._meta.get_field(field_name) | ||
if field_meta.is_relation: | ||
if _is_relation(field_meta): | ||
val_pk = getattr(inst, field_meta.attname) | ||
val_cls = field_meta.related_model | ||
val_expand_children = expand_children.get(field_name, {}) | ||
|
@@ -110,7 +144,7 @@ def _get_filtered_field_value( # noqa: C901 | |
pass | ||
|
||
val = getattr(inst, field_name) | ||
except (AttributeError, FieldDoesNotExist) as e: # noqa | ||
except (AttributeError, FieldDoesNotExist): | ||
return None | ||
|
||
if isinstance(val, models.Manager): | ||
|
@@ -122,11 +156,7 @@ def _get_filtered_field_value( # noqa: C901 | |
val, (list, tuple, models.Model, models.query.QuerySet) | ||
): | ||
val = _apply_filters_to_object( | ||
val, | ||
filter_def, | ||
expand_children=expand_children, | ||
klass=val.__class__, | ||
filter_cache=filter_cache, | ||
val, filter_def, expand_children, val.__class__, filter_cache, model_cache | ||
) | ||
|
||
if ( | ||
|
@@ -144,18 +174,17 @@ def _make_filter_cache_key(expand_children, klass, pk): | |
return (str(expand_children), klass, str(pk)) | ||
|
||
|
||
def _make_model_cache_key(klass, pk): | ||
return (klass, str(pk)) | ||
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. what will this key look like if we have to manually inspect it in cache? Can we have a more human readable cache key here, maybe something like 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. nevermind, i see it matches the style of the cache key of the other cache |
||
|
||
|
||
# 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, | ||
filter_cache=None, | ||
inst, filter_def, expand_children, klass, filter_cache, model_cache | ||
): | ||
is_cacheable = False | ||
caching_enabled = getattr(settings, "DDA_FILTER_MODEL_CACHING_ENABLED", False) | ||
if ( | ||
caching_enabled | ||
is_caching_enabled() | ||
and isinstance(inst, (models.Model,)) | ||
and filter_cache is not None | ||
): | ||
|
@@ -173,9 +202,10 @@ def _apply_filters_to_object( # noqa: C901 | |
_apply_filters_to_object( | ||
item, | ||
filter_def, | ||
expand_children=expand_children, | ||
klass=item.__class__, | ||
filter_cache=filter_cache, | ||
expand_children, | ||
item.__class__, | ||
filter_cache, | ||
model_cache, | ||
) | ||
for item in inst | ||
] | ||
|
@@ -184,9 +214,10 @@ def _apply_filters_to_object( # noqa: C901 | |
k: _apply_filters_to_object( | ||
v, | ||
filter_def, | ||
expand_children=expand_children, | ||
klass=v.__class__, | ||
filter_cache=filter_cache, | ||
expand_children, | ||
v.__class__, | ||
filter_cache, | ||
model_cache, | ||
) | ||
for k, v in inst.items() | ||
} | ||
|
@@ -200,9 +231,10 @@ def _apply_filters_to_object( # noqa: C901 | |
result = _apply_filters_to_object( | ||
inst, | ||
filter_def, | ||
expand_children=expand_children, | ||
klass=base_class, | ||
filter_cache=filter_cache, | ||
expand_children, | ||
base_class, | ||
filter_cache, | ||
model_cache, | ||
) | ||
break | ||
|
||
|
@@ -215,11 +247,7 @@ def _apply_filters_to_object( # noqa: C901 | |
result = defaultdict(list) | ||
for base in klass.__bases__: | ||
filtered_ancestor = _apply_filters_to_object( | ||
inst, | ||
filter_def, | ||
expand_children=expand_children, | ||
klass=base, | ||
filter_cache=filter_cache, | ||
inst, filter_def, expand_children, base, filter_cache, model_cache | ||
) | ||
if filtered_ancestor: | ||
result.update(filtered_ancestor) | ||
|
@@ -243,6 +271,7 @@ def _apply_filters_to_object( # noqa: C901 | |
expand_this=field_name in expand_children, | ||
expand_children=expand_children.get(field_name, {}), | ||
filter_cache=filter_cache, | ||
model_cache=model_cache, | ||
) | ||
|
||
if value is not None and value != DEFAULT_UNEXPANDED_VALUE: | ||
|
@@ -269,6 +298,10 @@ def _compile_expansion(expand_fields): | |
return top | ||
|
||
|
||
def is_caching_enabled(): | ||
return getattr(settings, "DDA_FILTER_MODEL_CACHING_ENABLED", False) | ||
|
||
|
||
def apply_filters_to_object(inst, filter_def, expand_header=""): | ||
if expand_header: | ||
expand_dict = _compile_expansion(expand_header.split(",")) | ||
|
@@ -280,4 +313,5 @@ def apply_filters_to_object(inst, filter_def, expand_header=""): | |
expand_children=expand_dict, | ||
klass=inst.__class__, | ||
filter_cache={}, | ||
model_cache={}, | ||
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. decided to add a new cache instead of reusing Filter cache using is storing
Model cache:
|
||
) |
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.
this adds support for fake relationships. In example
CharField
s acting as foreign keys.