Skip to content

Commit

Permalink
Performance improvement when rendering relationships with `ModelSeria…
Browse files Browse the repository at this point in the history
…lizer` (#461)
  • Loading branch information
timcbaoth authored and sliverc committed Sep 4, 2018
1 parent a320536 commit 22c4587
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 28 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ Oliver Sauder <[email protected]>
Raphael Cohen <[email protected]>
Roberto Barreda <[email protected]>
santiavenda <[email protected]>
Tim Selman <[email protected]>
Yaniv Peer <[email protected]>
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* Add optional [jsonapi-style](http://jsonapi.org/format/) sort filter backend. See [usage docs](docs/usage.md#filter-backends)
* For naming consistency, renamed new `JsonApi`-prefix pagination classes to `JSONAPI`-prefix.
* Deprecates `JsonApiPageNumberPagination` and `JsonApiLimitOffsetPagination`
* Performance improvement when rendering relationships with `ModelSerializer`


v2.5.0 - Released July 11, 2018

Expand Down
76 changes: 72 additions & 4 deletions example/tests/test_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,39 @@
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone

from rest_framework_json_api.serializers import ResourceIdentifierObjectSerializer
from rest_framework.request import Request
from rest_framework.test import APIRequestFactory

from rest_framework_json_api.serializers import (
DateField,
ModelSerializer,
ResourceIdentifierObjectSerializer
)
from rest_framework_json_api.utils import format_resource_type

from example.models import Author, Blog, Entry
from example.serializers import BlogSerializer

try:
from unittest import mock
except ImportError:
import mock

request_factory = APIRequestFactory()
pytestmark = pytest.mark.django_db


class TestResourceIdentifierObjectSerializer(TestCase):
def setUp(self):
self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog")
now = timezone.now()

self.entry = Entry.objects.create(
blog=self.blog,
headline='headline',
body_text='body_text',
pub_date=timezone.now(),
mod_date=timezone.now(),
pub_date=now.date(),
mod_date=now.date(),
n_comments=0,
n_pingbacks=0,
rating=3
Expand All @@ -30,6 +45,59 @@ def setUp(self):
Author.objects.create(name=name, email='{}@example.org'.format(name))
)

def test_forward_relationship_not_loaded_when_not_included(self):
to_representation_method = 'example.serializers.BlogSerializer.to_representation'
with mock.patch(to_representation_method) as mocked_serializer:
class EntrySerializer(ModelSerializer):
blog = BlogSerializer()

class Meta:
model = Entry
fields = '__all__'

request_without_includes = Request(request_factory.get('/'))
serializer = EntrySerializer(context={'request': request_without_includes})
serializer.to_representation(self.entry)

mocked_serializer.assert_not_called()

def test_forward_relationship_optimization_correct_representation(self):
class EntrySerializer(ModelSerializer):
blog = BlogSerializer()

class Meta:
model = Entry
fields = '__all__'

request_without_includes = Request(request_factory.get('/'))
serializer = EntrySerializer(context={'request': request_without_includes})
result = serializer.to_representation(self.entry)

# Remove non deterministic fields
result.pop('created_at')
result.pop('modified_at')

expected = dict(
[
('id', 1),
('blog', dict([('type', 'blogs'), ('id', 1)])),
('headline', 'headline'),
('body_text', 'body_text'),
('pub_date', DateField().to_representation(self.entry.pub_date)),
('mod_date', DateField().to_representation(self.entry.mod_date)),
('n_comments', 0),
('n_pingbacks', 0),
('rating', 3),
('authors',
[
dict([('type', 'authors'), ('id', '1')]),
dict([('type', 'authors'), ('id', '2')]),
dict([('type', 'authors'), ('id', '3')]),
dict([('type', 'authors'), ('id', '4')]),
dict([('type', 'authors'), ('id', '5')])])])

self.assertDictEqual(expected, result)

def test_data_in_correct_format_when_instantiated_with_blog_object(self):
serializer = ResourceIdentifierObjectSerializer(instance=self.blog)

Expand Down
54 changes: 30 additions & 24 deletions rest_framework_json_api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,35 +182,41 @@ def to_representation(self, instance):

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)
field_representation = self._get_field_representation(field, instance)
ret[field.field_name] = field_representation
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

def _get_field_representation(self, field, instance):
request = self.context.get('request')
is_included = field.source in get_included_resources(request)
if not is_included and \
isinstance(field, ModelSerializer) and \
hasattr(instance, field.source + '_id'):
attribute = getattr(instance, field.source + '_id')

if attribute is None:
return None

resource_type = get_resource_type_from_serializer(field)
if resource_type:
return OrderedDict([('type', resource_type), ('id', attribute)])

attribute = field.get_attribute(instance)

# 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:
return None
else:
return field.to_representation(attribute)


class PolymorphicSerializerMetaclass(SerializerMetaclass):
"""
Expand Down

0 comments on commit 22c4587

Please sign in to comment.