Skip to content

Commit 99925fb

Browse files
authored
API v3: optimize project listing (#12355)
This is reviving an old optimization that was added in 363dccb but it was added for the old footer API, but it works for API v3 as well. This used to be 24 queries, now it went down to 16.
1 parent 0737919 commit 99925fb

File tree

2 files changed

+50
-3
lines changed

2 files changed

+50
-3
lines changed

readthedocs/api/v3/tests/test_projects.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,39 @@ def test_projects_list(self):
3333
self._get_response_dict("projects-list"),
3434
)
3535

36+
def test_number_of_queries_projects_list(self):
37+
another_project = get(
38+
Project,
39+
users=[self.me],
40+
)
41+
superproject = get(
42+
Project,
43+
users=[self.me],
44+
)
45+
subproject = get(
46+
Project,
47+
users=[self.me],
48+
)
49+
superproject.add_subproject(subproject)
50+
51+
main_traslation = get(
52+
Project,
53+
users=[self.me],
54+
language="en",
55+
)
56+
translation = get(
57+
Project,
58+
users=[self.me],
59+
main_language_project=main_traslation,
60+
language="es",
61+
)
62+
url = reverse("projects-list")
63+
self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}")
64+
with self.assertNumQueries(16):
65+
response = self.client.get(url)
66+
assert response.status_code == 200
67+
assert len(response.json()["results"]) == 6
68+
3669
@override_settings(ALLOW_PRIVATE_REPOS=True)
3770
def test_projects_list_privacy_levels_enabled(self):
3871
self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}")

readthedocs/api/v3/views.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from django.contrib.contenttypes.models import ContentType
44
from django.db.models import Exists
55
from django.db.models import OuterRef
6+
from django.db.models import Prefetch
67
from rest_flex_fields import is_expanded
78
from rest_flex_fields.views import FlexFieldsMixin
89
from rest_framework import status
@@ -39,6 +40,7 @@
3940
from readthedocs.oauth.models import RemoteRepositoryRelation
4041
from readthedocs.organizations.models import Organization
4142
from readthedocs.organizations.models import Team
43+
from readthedocs.projects.models import Domain
4244
from readthedocs.projects.models import EnvironmentVariable
4345
from readthedocs.projects.models import Project
4446
from readthedocs.projects.models import ProjectRelationship
@@ -177,11 +179,23 @@ def get_queryset(self):
177179
# This could be a class attribute and managed on the ``ProjectQuerySetMixin`` in
178180
# case we want to extend the ``prefetch_related`` to other views as
179181
# well.
180-
return queryset.prefetch_related(
181-
"related_projects",
182-
"domains",
182+
return queryset.select_related(
183+
"main_language_project",
184+
).prefetch_related(
183185
"tags",
184186
"users",
187+
# Prefetch superprojects to avoid N+1 queries when serializing the project.
188+
Prefetch(
189+
"superprojects",
190+
ProjectRelationship.objects.all().select_related("parent"),
191+
to_attr="_superprojects",
192+
),
193+
# Prefetch the canonical domain to avoid N+1 queries when using the resolver.
194+
Prefetch(
195+
"domains",
196+
Domain.objects.filter(canonical=True),
197+
to_attr="_canonical_domains",
198+
),
185199
)
186200

187201
def create(self, request, *args, **kwargs):

0 commit comments

Comments
 (0)