Skip to content

Commit 858119e

Browse files
authored
Use PreloadIncludesMixin for ReadOnlyModelViewSet (#946)
1 parent 2eea772 commit 858119e

File tree

11 files changed

+106
-6
lines changed

11 files changed

+106
-6
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Jamie Bliss <[email protected]>
1414
Jason Housley <[email protected]>
1515
Jeppe Fihl-Pearson <[email protected]>
1616
Jerel Unruh <[email protected]>
17+
Jonas Metzener <[email protected]>
1718
Jonathan Senecal <[email protected]>
1819
Joseba Mendivil <[email protected]>
1920
Kevin Partington <[email protected]>

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
Note that in line with [Django REST Framework policy](http://www.django-rest-framework.org/topics/release-notes/),
99
any parts of the framework not mentioned in the documentation should generally be considered private API, and may be subject to change.
1010

11+
## Unreleased
12+
13+
### Fixed
14+
15+
* Include `PreloadIncludesMixin` in `ReadOnlyModelViewSet` to enable the usage of [performance utilities](https://django-rest-framework-json-api.readthedocs.io/en/stable/usage.html#performance-improvements) on read only views (regression since 2.8.0)
16+
1117
## [4.2.0] - 2021-05-12
1218

1319
### Added

docs/usage.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -942,7 +942,7 @@ class QuestSerializer(serializers.ModelSerializer):
942942

943943
Be aware that using included resources without any form of prefetching **WILL HURT PERFORMANCE** as it will introduce m\*(n+1) queries.
944944

945-
A viewset helper was therefore designed to automatically preload data when possible. Such is automatically available when subclassing `ModelViewSet`.
945+
A viewset helper was therefore designed to automatically preload data when possible. Such is automatically available when subclassing `ModelViewSet` or `ReadOnlyModelViewSet`.
946946

947947
It also allows to define custom `select_related` and `prefetch_related` for each requested `include` when needed in special cases:
948948

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Generated by Django 3.2.3 on 2021-05-26 03:32
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("example", "0008_labresults"),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name="labresults",
16+
name="author",
17+
field=models.ForeignKey(
18+
blank=True,
19+
null=True,
20+
on_delete=django.db.models.deletion.CASCADE,
21+
related_name="lab_results",
22+
to="example.author",
23+
),
24+
),
25+
]

example/models.py

+7
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,13 @@ class LabResults(models.Model):
161161
)
162162
date = models.DateField()
163163
measurements = models.TextField()
164+
author = models.ForeignKey(
165+
Author,
166+
null=True,
167+
blank=True,
168+
on_delete=models.CASCADE,
169+
related_name="lab_results",
170+
)
164171

165172

166173
class Company(models.Model):

example/serializers.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -356,9 +356,11 @@ class Meta:
356356

357357

358358
class LabResultsSerializer(serializers.ModelSerializer):
359+
included_serializers = {"author": AuthorSerializer}
360+
359361
class Meta:
360362
model = LabResults
361-
fields = ("date", "measurements")
363+
fields = ("date", "measurements", "author")
362364

363365

364366
class ProjectSerializer(serializers.PolymorphicModelSerializer):

example/tests/test_performance.py

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
from datetime import date, timedelta
2+
from random import randint
3+
14
from django.utils import timezone
25
from rest_framework.test import APITestCase
36

47
from example.factories import CommentFactory, EntryFactory
5-
from example.models import Author, Blog, Comment, Entry
8+
from example.models import Author, Blog, Comment, Entry, LabResults, ResearchProject
69

710

811
class PerformanceTestCase(APITestCase):
@@ -84,3 +87,32 @@ def test_query_prefetch_uses_included_resources(self):
8487
"/entries?fields[entries]=comments&page[size]=25"
8588
)
8689
self.assertEqual(len(response.data["results"]), 25)
90+
91+
def test_query_prefetch_read_only(self):
92+
"""We expect a read only list view with an include have five queries:
93+
94+
1. Primary resource COUNT query
95+
2. Primary resource SELECT
96+
3. Authors prefetched
97+
4. Author types prefetched
98+
5. Entries prefetched
99+
"""
100+
project = ResearchProject.objects.create(
101+
topic="Mars Mission", supervisor="Elon Musk"
102+
)
103+
104+
LabResults.objects.bulk_create(
105+
[
106+
LabResults(
107+
research_project=project,
108+
date=date.today() + timedelta(days=i),
109+
measurements=randint(0, 10000),
110+
author=self.author,
111+
)
112+
for i in range(20)
113+
]
114+
)
115+
116+
with self.assertNumQueries(5):
117+
response = self.client.get("/lab-results?include=author&page[size]=25")
118+
self.assertEqual(len(response.data["results"]), 20)

example/urls.py

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
CompanyViewset,
1818
EntryRelationshipView,
1919
EntryViewSet,
20+
LabResultViewSet,
2021
NonPaginatedEntryViewSet,
2122
ProjectTypeViewset,
2223
ProjectViewset,
@@ -32,6 +33,7 @@
3233
router.register(r"companies", CompanyViewset)
3334
router.register(r"projects", ProjectViewset)
3435
router.register(r"project-types", ProjectTypeViewset)
36+
router.register(r"lab-results", LabResultViewSet)
3537

3638
urlpatterns = [
3739
url(r"^", include(router.urls)),

example/urls_test.py

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
EntryRelationshipView,
1616
EntryViewSet,
1717
FiltersetEntryViewSet,
18+
LabResultViewSet,
1819
NoFiltersetEntryViewSet,
1920
NonPaginatedEntryViewSet,
2021
ProjectTypeViewset,
@@ -36,6 +37,7 @@
3637
router.register(r"companies", CompanyViewset)
3738
router.register(r"projects", ProjectViewset)
3839
router.register(r"project-types", ProjectTypeViewset)
40+
router.register(r"lab-results", LabResultViewSet)
3941

4042
# for the old tests
4143
router.register(r"identities", Identity)

example/views.py

+25-2
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,22 @@
1414
)
1515
from rest_framework_json_api.pagination import JsonApiPageNumberPagination
1616
from rest_framework_json_api.utils import format_drf_errors
17-
from rest_framework_json_api.views import ModelViewSet, RelationshipView
17+
from rest_framework_json_api.views import (
18+
ModelViewSet,
19+
ReadOnlyModelViewSet,
20+
RelationshipView,
21+
)
1822

19-
from example.models import Author, Blog, Comment, Company, Entry, Project, ProjectType
23+
from example.models import (
24+
Author,
25+
Blog,
26+
Comment,
27+
Company,
28+
Entry,
29+
LabResults,
30+
Project,
31+
ProjectType,
32+
)
2033
from example.serializers import (
2134
AuthorDetailSerializer,
2235
AuthorListSerializer,
@@ -27,6 +40,7 @@
2740
CompanySerializer,
2841
EntryDRFSerializers,
2942
EntrySerializer,
43+
LabResultsSerializer,
3044
ProjectSerializer,
3145
ProjectTypeSerializer,
3246
)
@@ -266,3 +280,12 @@ class CommentRelationshipView(RelationshipView):
266280
class AuthorRelationshipView(RelationshipView):
267281
queryset = Author.objects.all()
268282
self_link_view_name = "author-relationships"
283+
284+
285+
class LabResultViewSet(ReadOnlyModelViewSet):
286+
queryset = LabResults.objects.all()
287+
serializer_class = LabResultsSerializer
288+
prefetch_for_includes = {
289+
"__all__": [],
290+
"author": ["author__bio", "author__entries"],
291+
}

rest_framework_json_api/views.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ class ModelViewSet(
224224

225225

226226
class ReadOnlyModelViewSet(
227-
AutoPrefetchMixin, RelatedMixin, viewsets.ReadOnlyModelViewSet
227+
AutoPrefetchMixin, PreloadIncludesMixin, RelatedMixin, viewsets.ReadOnlyModelViewSet
228228
):
229229
http_method_names = ["get", "post", "patch", "delete", "head", "options"]
230230

0 commit comments

Comments
 (0)