-
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
Conversation
Thanks for the contribution! It looks like @lukka5 is an internal user so signing the CLA is not required. However, we need to confirm this. |
@@ -280,4 +311,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 comment
The reason will be displayed to describe this comment to others. Learn more.
decided to add a new cache instead of reusing filter_cache
since they key/values types are completely different from filter cache.
Filter cache using is storing
- key: (expandables, class, pk)
- value: defaultdict(...) with filtered/formatted results
Model cache:
- key: (class, pk)
- value: model instances
logger.debug("ev=model_cache, status=miss, key=%s", cache_key) | ||
related_instance = getattr(inst, field_name) | ||
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 comment
The 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 counter
has already been populated by a counter from cache or stored for later used by another filter function.
@@ -67,6 +68,34 @@ 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) |
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.
4bfb611
to
4011413
Compare
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Seems very generic, can't we use the instance type of val_cls
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.
good catch. yeah, I like it
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.
added it
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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is that field_name
is a consumer defined string for specifying the output field name after filtering it. So it can have aj different name of the actual field. For example:
FILTERS = {
'my_increased_counter': lambda inst: inst.counter.count + 1
}
Here field_name
would be my_increased_counter
which doesn't exists in the model.
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.
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 comment
The 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 my_increased_counter
as field name he won't get caching? In that case I'm not sure how much useful it is but yeah we could add some logging
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.
I think I understand what you're saying now. This PR looks good, approved!
return | ||
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 comment
The 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 comment
The 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 attname
but are not relations so we cannot cache them
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 ok.
@@ -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 comment
The 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 {klass.__cache__.__name}:{str(pk)}
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.
nevermind, i see it matches the style of the cache key of the other cache
pyproject.toml
pip install
succeeds with a clean virtualenv