Skip to content

Commit 22c4587

Browse files
timcbaothsliverc
authored andcommitted
Performance improvement when rendering relationships with ModelSerializer (#461)
1 parent a320536 commit 22c4587

File tree

4 files changed

+105
-28
lines changed

4 files changed

+105
-28
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ Oliver Sauder <[email protected]>
1515
Raphael Cohen <[email protected]>
1616
Roberto Barreda <[email protected]>
1717
santiavenda <[email protected]>
18+
Tim Selman <[email protected]>
1819
Yaniv Peer <[email protected]>

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
* Add optional [jsonapi-style](http://jsonapi.org/format/) sort filter backend. See [usage docs](docs/usage.md#filter-backends)
88
* For naming consistency, renamed new `JsonApi`-prefix pagination classes to `JSONAPI`-prefix.
99
* Deprecates `JsonApiPageNumberPagination` and `JsonApiLimitOffsetPagination`
10+
* Performance improvement when rendering relationships with `ModelSerializer`
11+
1012

1113
v2.5.0 - Released July 11, 2018
1214

example/tests/test_serializers.py

+72-4
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,39 @@
22
from django.test import TestCase
33
from django.urls import reverse
44
from django.utils import timezone
5-
6-
from rest_framework_json_api.serializers import ResourceIdentifierObjectSerializer
5+
from rest_framework.request import Request
6+
from rest_framework.test import APIRequestFactory
7+
8+
from rest_framework_json_api.serializers import (
9+
DateField,
10+
ModelSerializer,
11+
ResourceIdentifierObjectSerializer
12+
)
713
from rest_framework_json_api.utils import format_resource_type
814

915
from example.models import Author, Blog, Entry
16+
from example.serializers import BlogSerializer
17+
18+
try:
19+
from unittest import mock
20+
except ImportError:
21+
import mock
1022

23+
request_factory = APIRequestFactory()
1124
pytestmark = pytest.mark.django_db
1225

1326

1427
class TestResourceIdentifierObjectSerializer(TestCase):
1528
def setUp(self):
1629
self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog")
30+
now = timezone.now()
31+
1732
self.entry = Entry.objects.create(
1833
blog=self.blog,
1934
headline='headline',
2035
body_text='body_text',
21-
pub_date=timezone.now(),
22-
mod_date=timezone.now(),
36+
pub_date=now.date(),
37+
mod_date=now.date(),
2338
n_comments=0,
2439
n_pingbacks=0,
2540
rating=3
@@ -30,6 +45,59 @@ def setUp(self):
3045
Author.objects.create(name=name, email='{}@example.org'.format(name))
3146
)
3247

48+
def test_forward_relationship_not_loaded_when_not_included(self):
49+
to_representation_method = 'example.serializers.BlogSerializer.to_representation'
50+
with mock.patch(to_representation_method) as mocked_serializer:
51+
class EntrySerializer(ModelSerializer):
52+
blog = BlogSerializer()
53+
54+
class Meta:
55+
model = Entry
56+
fields = '__all__'
57+
58+
request_without_includes = Request(request_factory.get('/'))
59+
serializer = EntrySerializer(context={'request': request_without_includes})
60+
serializer.to_representation(self.entry)
61+
62+
mocked_serializer.assert_not_called()
63+
64+
def test_forward_relationship_optimization_correct_representation(self):
65+
class EntrySerializer(ModelSerializer):
66+
blog = BlogSerializer()
67+
68+
class Meta:
69+
model = Entry
70+
fields = '__all__'
71+
72+
request_without_includes = Request(request_factory.get('/'))
73+
serializer = EntrySerializer(context={'request': request_without_includes})
74+
result = serializer.to_representation(self.entry)
75+
76+
# Remove non deterministic fields
77+
result.pop('created_at')
78+
result.pop('modified_at')
79+
80+
expected = dict(
81+
[
82+
('id', 1),
83+
('blog', dict([('type', 'blogs'), ('id', 1)])),
84+
('headline', 'headline'),
85+
('body_text', 'body_text'),
86+
('pub_date', DateField().to_representation(self.entry.pub_date)),
87+
('mod_date', DateField().to_representation(self.entry.mod_date)),
88+
('n_comments', 0),
89+
('n_pingbacks', 0),
90+
('rating', 3),
91+
('authors',
92+
[
93+
dict([('type', 'authors'), ('id', '1')]),
94+
dict([('type', 'authors'), ('id', '2')]),
95+
dict([('type', 'authors'), ('id', '3')]),
96+
dict([('type', 'authors'), ('id', '4')]),
97+
dict([('type', 'authors'), ('id', '5')])])])
98+
99+
self.assertDictEqual(expected, result)
100+
33101
def test_data_in_correct_format_when_instantiated_with_blog_object(self):
34102
serializer = ResourceIdentifierObjectSerializer(instance=self.blog)
35103

rest_framework_json_api/serializers.py

+30-24
Original file line numberDiff line numberDiff line change
@@ -182,35 +182,41 @@ def to_representation(self, instance):
182182

183183
for field in readable_fields:
184184
try:
185-
186-
if isinstance(field, ModelSerializer) and hasattr(field, field.source + "_id"):
187-
attribute = getattr(instance, field.source + "_id")
188-
if attribute is None:
189-
ret[field.field_name] = None
190-
continue
191-
resource_type = get_resource_type_from_instance(field)
192-
if resource_type:
193-
ret[field.field_name] = OrderedDict([("type", resource_type),
194-
("id", attribute)])
195-
continue
196-
197-
attribute = field.get_attribute(instance)
185+
field_representation = self._get_field_representation(field, instance)
186+
ret[field.field_name] = field_representation
198187
except SkipField:
199188
continue
200189

201-
# We skip `to_representation` for `None` values so that fields do
202-
# not have to explicitly deal with that case.
203-
#
204-
# For related fields with `use_pk_only_optimization` we need to
205-
# resolve the pk value.
206-
check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
207-
if check_for_none is None:
208-
ret[field.field_name] = None
209-
else:
210-
ret[field.field_name] = field.to_representation(attribute)
211-
212190
return ret
213191

192+
def _get_field_representation(self, field, instance):
193+
request = self.context.get('request')
194+
is_included = field.source in get_included_resources(request)
195+
if not is_included and \
196+
isinstance(field, ModelSerializer) and \
197+
hasattr(instance, field.source + '_id'):
198+
attribute = getattr(instance, field.source + '_id')
199+
200+
if attribute is None:
201+
return None
202+
203+
resource_type = get_resource_type_from_serializer(field)
204+
if resource_type:
205+
return OrderedDict([('type', resource_type), ('id', attribute)])
206+
207+
attribute = field.get_attribute(instance)
208+
209+
# We skip `to_representation` for `None` values so that fields do
210+
# not have to explicitly deal with that case.
211+
#
212+
# For related fields with `use_pk_only_optimization` we need to
213+
# resolve the pk value.
214+
check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
215+
if check_for_none is None:
216+
return None
217+
else:
218+
return field.to_representation(attribute)
219+
214220

215221
class PolymorphicSerializerMetaclass(SerializerMetaclass):
216222
"""

0 commit comments

Comments
 (0)