Skip to content

Use PreloadIncludesMixin for ReadOnlyModelViewSet #946

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 2 commits into from
May 28, 2021
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
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Jamie Bliss <[email protected]>
Jason Housley <[email protected]>
Jeppe Fihl-Pearson <[email protected]>
Jerel Unruh <[email protected]>
Jonas Metzener <[email protected]>
Jonathan Senecal <[email protected]>
Joseba Mendivil <[email protected]>
Kevin Partington <[email protected]>
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
25 changes: 25 additions & 0 deletions example/migrations/0009_labresults_author.py
Original file line number Diff line number Diff line change
@@ -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",
),
),
]
7 changes: 7 additions & 0 deletions example/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 3 additions & 1 deletion example/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
34 changes: 33 additions & 1 deletion example/tests/test_performance.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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)
2 changes: 2 additions & 0 deletions example/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
CompanyViewset,
EntryRelationshipView,
EntryViewSet,
LabResultViewSet,
NonPaginatedEntryViewSet,
ProjectTypeViewset,
ProjectViewset,
Expand All @@ -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)),
Expand Down
2 changes: 2 additions & 0 deletions example/urls_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
EntryRelationshipView,
EntryViewSet,
FiltersetEntryViewSet,
LabResultViewSet,
NoFiltersetEntryViewSet,
NonPaginatedEntryViewSet,
ProjectTypeViewset,
Expand All @@ -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)
Expand Down
27 changes: 25 additions & 2 deletions example/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -27,6 +40,7 @@
CompanySerializer,
EntryDRFSerializers,
EntrySerializer,
LabResultsSerializer,
ProjectSerializer,
ProjectTypeSerializer,
)
Expand Down Expand Up @@ -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"],
}
2 changes: 1 addition & 1 deletion rest_framework_json_api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down