diff --git a/AUTHORS b/AUTHORS index 500dc3c6..8d2af17d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -14,6 +14,7 @@ Jamie Bliss Jason Housley Jeppe Fihl-Pearson Jerel Unruh +Jonas Metzener Jonathan Senecal Joseba Mendivil Kevin Partington diff --git a/CHANGELOG.md b/CHANGELOG.md index d7155df3..508da91f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Note that in line with [Django REST Framework policy](http://www.django-rest-framework.org/topics/release-notes/), any parts of the framework not mentioned in the documentation should generally be considered private API, and may be subject to change. +## Unreleased + +### Fixed + +* 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) + ## [4.2.0] - 2021-05-12 ### Added diff --git a/docs/usage.md b/docs/usage.md index 79a2d918..1e45976b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -942,7 +942,7 @@ class QuestSerializer(serializers.ModelSerializer): Be aware that using included resources without any form of prefetching **WILL HURT PERFORMANCE** as it will introduce m\*(n+1) queries. -A viewset helper was therefore designed to automatically preload data when possible. Such is automatically available when subclassing `ModelViewSet`. +A viewset helper was therefore designed to automatically preload data when possible. Such is automatically available when subclassing `ModelViewSet` or `ReadOnlyModelViewSet`. It also allows to define custom `select_related` and `prefetch_related` for each requested `include` when needed in special cases: diff --git a/example/migrations/0009_labresults_author.py b/example/migrations/0009_labresults_author.py new file mode 100644 index 00000000..6365d01c --- /dev/null +++ b/example/migrations/0009_labresults_author.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.3 on 2021-05-26 03:32 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("example", "0008_labresults"), + ] + + operations = [ + migrations.AddField( + model_name="labresults", + name="author", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="lab_results", + to="example.author", + ), + ), + ] diff --git a/example/models.py b/example/models.py index 47537b57..c3785c27 100644 --- a/example/models.py +++ b/example/models.py @@ -161,6 +161,13 @@ class LabResults(models.Model): ) date = models.DateField() measurements = models.TextField() + author = models.ForeignKey( + Author, + null=True, + blank=True, + on_delete=models.CASCADE, + related_name="lab_results", + ) class Company(models.Model): diff --git a/example/serializers.py b/example/serializers.py index 4d80c87c..64444e9e 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -356,9 +356,11 @@ class Meta: class LabResultsSerializer(serializers.ModelSerializer): + included_serializers = {"author": AuthorSerializer} + class Meta: model = LabResults - fields = ("date", "measurements") + fields = ("date", "measurements", "author") class ProjectSerializer(serializers.PolymorphicModelSerializer): diff --git a/example/tests/test_performance.py b/example/tests/test_performance.py index cae11ed4..ec4a81b2 100644 --- a/example/tests/test_performance.py +++ b/example/tests/test_performance.py @@ -1,8 +1,11 @@ +from datetime import date, timedelta +from random import randint + from django.utils import timezone from rest_framework.test import APITestCase from example.factories import CommentFactory, EntryFactory -from example.models import Author, Blog, Comment, Entry +from example.models import Author, Blog, Comment, Entry, LabResults, ResearchProject class PerformanceTestCase(APITestCase): @@ -84,3 +87,32 @@ def test_query_prefetch_uses_included_resources(self): "/entries?fields[entries]=comments&page[size]=25" ) self.assertEqual(len(response.data["results"]), 25) + + def test_query_prefetch_read_only(self): + """We expect a read only list view with an include have five queries: + + 1. Primary resource COUNT query + 2. Primary resource SELECT + 3. Authors prefetched + 4. Author types prefetched + 5. Entries prefetched + """ + project = ResearchProject.objects.create( + topic="Mars Mission", supervisor="Elon Musk" + ) + + LabResults.objects.bulk_create( + [ + LabResults( + research_project=project, + date=date.today() + timedelta(days=i), + measurements=randint(0, 10000), + author=self.author, + ) + for i in range(20) + ] + ) + + with self.assertNumQueries(5): + response = self.client.get("/lab-results?include=author&page[size]=25") + self.assertEqual(len(response.data["results"]), 20) diff --git a/example/urls.py b/example/urls.py index 800b9f79..867f1bd7 100644 --- a/example/urls.py +++ b/example/urls.py @@ -17,6 +17,7 @@ CompanyViewset, EntryRelationshipView, EntryViewSet, + LabResultViewSet, NonPaginatedEntryViewSet, ProjectTypeViewset, ProjectViewset, @@ -32,6 +33,7 @@ router.register(r"companies", CompanyViewset) router.register(r"projects", ProjectViewset) router.register(r"project-types", ProjectTypeViewset) +router.register(r"lab-results", LabResultViewSet) urlpatterns = [ url(r"^", include(router.urls)), diff --git a/example/urls_test.py b/example/urls_test.py index 0219ac51..5ee06a23 100644 --- a/example/urls_test.py +++ b/example/urls_test.py @@ -15,6 +15,7 @@ EntryRelationshipView, EntryViewSet, FiltersetEntryViewSet, + LabResultViewSet, NoFiltersetEntryViewSet, NonPaginatedEntryViewSet, ProjectTypeViewset, @@ -36,6 +37,7 @@ router.register(r"companies", CompanyViewset) router.register(r"projects", ProjectViewset) router.register(r"project-types", ProjectTypeViewset) +router.register(r"lab-results", LabResultViewSet) # for the old tests router.register(r"identities", Identity) diff --git a/example/views.py b/example/views.py index 6a1b15a6..0b35d4e4 100644 --- a/example/views.py +++ b/example/views.py @@ -14,9 +14,22 @@ ) from rest_framework_json_api.pagination import JsonApiPageNumberPagination from rest_framework_json_api.utils import format_drf_errors -from rest_framework_json_api.views import ModelViewSet, RelationshipView +from rest_framework_json_api.views import ( + ModelViewSet, + ReadOnlyModelViewSet, + RelationshipView, +) -from example.models import Author, Blog, Comment, Company, Entry, Project, ProjectType +from example.models import ( + Author, + Blog, + Comment, + Company, + Entry, + LabResults, + Project, + ProjectType, +) from example.serializers import ( AuthorDetailSerializer, AuthorListSerializer, @@ -27,6 +40,7 @@ CompanySerializer, EntryDRFSerializers, EntrySerializer, + LabResultsSerializer, ProjectSerializer, ProjectTypeSerializer, ) @@ -266,3 +280,12 @@ class CommentRelationshipView(RelationshipView): class AuthorRelationshipView(RelationshipView): queryset = Author.objects.all() self_link_view_name = "author-relationships" + + +class LabResultViewSet(ReadOnlyModelViewSet): + queryset = LabResults.objects.all() + serializer_class = LabResultsSerializer + prefetch_for_includes = { + "__all__": [], + "author": ["author__bio", "author__entries"], + } diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index e457009b..bb6f09bb 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -224,7 +224,7 @@ class ModelViewSet( class ReadOnlyModelViewSet( - AutoPrefetchMixin, RelatedMixin, viewsets.ReadOnlyModelViewSet + AutoPrefetchMixin, PreloadIncludesMixin, RelatedMixin, viewsets.ReadOnlyModelViewSet ): http_method_names = ["get", "post", "patch", "delete", "head", "options"]