Skip to content

Avoid query explosion for related fields where I can user pk_only_opt… #374

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

Merged
merged 15 commits into from
Jan 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions example/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,11 @@ class AuthorViewSet(ModelViewSet):


class CommentViewSet(ModelViewSet):
queryset = Comment.objects.select_related('author', 'entry')
queryset = Comment.objects.all()
serializer_class = CommentSerializer
prefetch_for_includes = {
'__all__': [],
'author': ['author', 'author__bio', 'author__entries', 'author__type'],
'entry': ['author', 'author__bio', 'author__entries']
'author': ['author__bio', 'author__entries'],
}


Expand Down
2 changes: 1 addition & 1 deletion requirements-development.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ Sphinx
sphinx_rtd_theme
tox
django-debug-toolbar
packaging==16.8
packaging==16.8
5 changes: 4 additions & 1 deletion rest_framework_json_api/relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def __init__(self, self_link_view_name=None, related_link_view_name=None, **kwar

def use_pk_only_optimization(self):
# We need the real object to determine its type...
return False
return self.get_resource_type_from_included_serializer() is not None

def conflict(self, key, **kwargs):
"""
Expand Down Expand Up @@ -251,6 +251,9 @@ def __init__(self, polymorphic_serializer, *args, **kwargs):
self.polymorphic_serializer = polymorphic_serializer
super(PolymorphicResourceRelatedField, self).__init__(*args, **kwargs)

def use_pk_only_optimization(self):
return False

def to_internal_value(self, data):
if isinstance(data, six.text_type):
try:
Expand Down
28 changes: 17 additions & 11 deletions rest_framework_json_api/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,12 @@ def extract_relationships(cls, fields, resource, resource_instance):
continue

if isinstance(field, ResourceRelatedField):
resolved, relation_instance = utils.get_relation_instance(
resource_instance, source, field.parent
)
if not resolved:
continue
relation_instance_id = getattr(resource_instance, source + "_id", None)
if not relation_instance_id:
resolved, relation_instance = utils.get_relation_instance(resource_instance,
source, field.parent)
if not resolved:
continue

# special case for ResourceRelatedField
relation_data = {
Expand Down Expand Up @@ -256,18 +257,23 @@ def extract_relationships(cls, fields, resource, resource_instance):
continue

if isinstance(field, Serializer):
resolved, relation_instance = utils.get_relation_instance(
resource_instance, source, field.parent
)
if not resolved:
continue
relation_instance_id = getattr(resource_instance, source + "_id", None)
if not relation_instance_id:
resolved, relation_instance = utils.get_relation_instance(
resource_instance, source, field.parent
)
if not resolved:
continue

if relation_instance is not None:
relation_instance_id = relation_instance.pk

data.update({
field_name: {
'data': (
OrderedDict([
('type', relation_type),
('id', encoding.force_text(relation_instance.pk))
('id', encoding.force_text(relation_instance_id))
]) if resource.get(field_name) else None)
}
})
Expand Down
41 changes: 41 additions & 0 deletions rest_framework_json_api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,47 @@ def get_field_names(self, declared_fields, info):
fields = super(ModelSerializer, self).get_field_names(declared, info)
return list(fields) + list(getattr(self.Meta, 'meta_fields', list()))

def to_representation(self, instance):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain why this is overriding to_representation from the ModelSerializer? It seems like DJA is likely to diverge from the DRF upstream if this method is added. Is there some extra stuff added here that you could call out?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mblayman this is returning the JSON API identifier of the object without diving down the recursive rabbit whole. It looks like an elegant solution.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the second look, @aidanlister. I took a look at the DRF implementation and the method seems small enough that it would be hard for DJA to diverge too much from upstream.

"""
Object instance -> Dict of primitive datatypes.
"""
ret = OrderedDict()
readable_fields = [
field for field in self.fields.values()
if not field.write_only
]

for field in readable_fields:
try:

if isinstance(field, ModelSerializer) and hasattr(field, field.source + "_id"):
attribute = getattr(instance, field.source + "_id")
if attribute is None:
ret[field.field_name] = None
continue
resource_type = get_resource_type_from_instance(field)
if resource_type:
ret[field.field_name] = OrderedDict([("type", resource_type),
("id", attribute)])
continue

attribute = field.get_attribute(instance)
except SkipField:
continue

# We skip `to_representation` for `None` values so that fields do
# not have to explicitly deal with that case.
#
# For related fields with `use_pk_only_optimization` we need to
# resolve the pk value.
check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
if check_for_none is None:
ret[field.field_name] = None
else:
ret[field.field_name] = field.to_representation(attribute)

return ret


class PolymorphicSerializerMetaclass(SerializerMetaclass):
"""
Expand Down
4 changes: 2 additions & 2 deletions rest_framework_json_api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,12 +310,12 @@ def get_relation_instance(resource_instance, source, serializer):
if serializer_method and hasattr(serializer_method, '__call__'):
relation_instance = serializer_method(resource_instance)
else:
return (False, None)
return False, None

if isinstance(relation_instance, Manager):
relation_instance = relation_instance.all()

return (True, relation_instance)
return True, relation_instance


class Hyperlink(six.text_type):
Expand Down