Skip to content

Commit f8e9897

Browse files
committed
Remove n+1 queries
1 parent 00f7540 commit f8e9897

18 files changed

Lines changed: 395 additions & 49 deletions

onadata/apps/api/models/temp_token.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""
33
Temporary token authorization model class
44
"""
5+
56
import binascii
67
import os
78

@@ -10,14 +11,13 @@
1011

1112

1213
class TempToken(models.Model):
13-
1414
"""
1515
The temporary authorization token model.
1616
"""
1717

1818
key = models.CharField(max_length=40, primary_key=True)
1919
user = models.OneToOneField(
20-
get_user_model(), related_name="_user", on_delete=models.CASCADE
20+
get_user_model(), related_name="temptoken", on_delete=models.CASCADE
2121
)
2222
created = models.DateTimeField(auto_now_add=True)
2323

onadata/apps/api/tools.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -295,13 +295,15 @@ def get_organization_members(organization):
295295
"""Get members team user queryset"""
296296
team = get_organization_members_team(organization)
297297

298-
return team.user_set.filter(is_active=True)
298+
# Optimize: Prefetch profile to avoid N+1
299+
return team.user_set.filter(is_active=True).select_related("profile")
299300

300301

301302
def get_organization_owners(organization):
302303
"""Get owners team user queryset"""
303304
team = get_or_create_organization_owners_team(organization)
304-
return team.user_set.filter(is_active=True)
305+
# Optimize: Prefetch profile to avoid N+1
306+
return team.user_set.filter(is_active=True).select_related("profile")
305307

306308

307309
def _get_owners(organization):
@@ -655,18 +657,20 @@ def check_inherit_permission_from_project(xform_id, user):
655657
return
656658

657659
# get the project_xform
660+
# Only fetch the fields we need: xform.id, xform.project_id, and
661+
# project.id for permissions
658662
xform = (
659663
XForm.objects.filter(pk=xform_id)
660664
.select_related("project")
661-
.only("project_id", "id")
665+
.only("project_id", "id", "project__id")
662666
.first()
663667
)
664668

665669
if not xform:
666670
return
667671

668672
# ignore if forms has meta perms set
669-
if xform.metadata_set.filter(data_type=XFORM_META_PERMS):
673+
if xform.metadata_set.filter(data_type=XFORM_META_PERMS).exists():
670674
return
671675

672676
# get and compare the project role to the xform role

onadata/apps/api/viewsets/attachment_viewset.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class AttachmentViewSet(
5959
lookup_field = "pk"
6060
queryset = Attachment.objects.filter(
6161
instance__deleted_at__isnull=True, deleted_at__isnull=True
62-
)
62+
).select_related("instance__xform", "xform")
6363
permission_classes = (AttachmentObjectPermissions,)
6464
serializer_class = AttachmentSerializer
6565
pagination_class = StandardPageNumberPagination

onadata/apps/api/viewsets/connect_viewset.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ def user_profile_w_token_response(request, status_code):
5252
session.set_expiry(DEFAULT_SESSION_EXPIRY_TIME)
5353

5454
try:
55-
user_profile = request.user.profile
55+
user_profile = UserProfile.objects.select_related(
56+
"user", "user__auth_token", "user__temptoken"
57+
).get(user=request.user)
5658
except UserProfile.DoesNotExist:
5759
user_profile = safe_cache_get(f"{USER_PROFILE_PREFIX}{request.user.username}")
5860
if not user_profile:

onadata/apps/api/viewsets/data_viewset.py

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ class DataViewSet(
156156

157157
queryset = XForm.objects.filter(deleted_at__isnull=True)
158158

159+
def __init__(self, *args, **kwargs):
160+
super().__init__(*args, **kwargs)
161+
self._cached_object = None
162+
159163
def get_serializer_class(self):
160164
"""Returns appropriate serializer class based on context."""
161165
pk_lookup, dataid_lookup = self.lookup_fields
@@ -187,6 +191,10 @@ def get_serializer_class(self):
187191
# pylint: disable=unused-argument
188192
def get_object(self, queryset=None):
189193
"""Returns the appropriate object based on context."""
194+
# Cache the object to avoid multiple queries in the same request
195+
if self._cached_object is None:
196+
self._cached_object = super().get_object()
197+
obj = self._cached_object
190198
pk_lookup, dataid_lookup = self.lookup_fields
191199
form_pk = self.kwargs.get(pk_lookup)
192200
dataid = self.kwargs.get(dataid_lookup)
@@ -227,21 +235,33 @@ def _get_public_forms_queryset(self):
227235

228236
def _filtered_or_shared_queryset(self, queryset, form_pk):
229237
filter_kwargs = {self.lookup_field: form_pk}
230-
queryset = queryset.filter(**filter_kwargs).only("id", "shared")
238+
# Don't override .only() - let the queryset keep its field restrictions
239+
queryset = queryset.filter(**filter_kwargs)
231240

232-
if not queryset:
241+
# Use .exists() to avoid executing the full query twice
242+
if not queryset.exists():
233243
filter_kwargs["shared_data"] = True
234-
queryset = XForm.objects.filter(**filter_kwargs).only("id", "shared")
244+
# Use the same field restrictions as the parent filter_queryset
245+
queryset = XForm.objects.filter(**filter_kwargs).only(
246+
"id", "shared", "num_of_submissions", "json", "is_merged_dataset"
247+
)
235248

236-
if not queryset:
249+
if not queryset.exists():
237250
raise Http404(_("No data matches with given query."))
238251

239252
return queryset
240253

241254
# pylint: disable=unused-argument
242255
def filter_queryset(self, queryset, view=None):
243256
"""Returns and filters queryset based on context and query params."""
244-
queryset = super().filter_queryset(queryset.only("id", "shared"))
257+
# Fetch only the fields we need: id, shared (for permissions),
258+
# num_of_submissions (for pagination), json (for survey structure),
259+
# and is_merged_dataset (for query building)
260+
queryset = super().filter_queryset(
261+
queryset.only(
262+
"id", "shared", "num_of_submissions", "json", "is_merged_dataset"
263+
)
264+
)
245265
form_pk = self.kwargs.get(self.lookup_field)
246266

247267
if form_pk:
@@ -578,10 +598,12 @@ def list(self, request, *args, **kwargs):
578598
# pylint: disable=attribute-defined-outside-init
579599
self.object_list = self._get_public_forms_queryset()
580600
elif lookup:
581-
queryset = self.filter_queryset(self.get_queryset()).values_list(
582-
"pk", "is_merged_dataset"
583-
)
584-
xform_id, is_merged_dataset = queryset[0] if queryset else (lookup, False)
601+
# Use get_object() to leverage caching and maintain permission
602+
# checks. This replaces the separate filter_queryset() call to
603+
# avoid duplicate queries
604+
xform = self.get_object()
605+
xform_id = xform.id
606+
is_merged_dataset = xform.is_merged_dataset
585607
pks = [xform_id]
586608
if is_merged_dataset:
587609
merged_form = MergedXForm.objects.get(pk=xform_id)
@@ -594,9 +616,9 @@ def list(self, request, *args, **kwargs):
594616
except ValueError:
595617
pks, num_of_submissions = [], 0
596618
else:
597-
num_of_submissions = XForm.objects.get(id=xform_id).num_of_submissions
619+
num_of_submissions = xform.num_of_submissions
598620
# pylint: disable=attribute-defined-outside-init
599-
# Include geom and xform_id fields for geojson format to avoid N+1 queries
621+
# For GeoJSON, we need id, json, xform_id(for xform relationship), and geom
600622
if export_type == "geojson":
601623
self.object_list = Instance.objects.filter(
602624
xform_id__in=pks, deleted_at=None

onadata/apps/api/viewsets/merged_xform_viewset.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class MergedXFormViewSet(
4242
queryset = (
4343
MergedXForm.objects.filter(deleted_at__isnull=True)
4444
.annotate(number_of_submissions=Sum("xforms__num_of_submissions"))
45+
.prefetch_related("xforms__user", "xforms__project")
4546
.all()
4647
)
4748
pagination_class = StandardPageNumberPagination
@@ -51,6 +52,29 @@ class MergedXFormViewSet(
5152
renderers.GeoJsonRenderer,
5253
]
5354

55+
def __init__(self, *args, **kwargs):
56+
super().__init__(*args, **kwargs)
57+
self._cached_object = None
58+
59+
def get_object(self):
60+
"""
61+
Get object - cache to avoid multiple queries during request
62+
"""
63+
if self._cached_object is None:
64+
self._cached_object = super().get_object()
65+
return self._cached_object
66+
67+
def get_queryset(self):
68+
"""
69+
Get queryset - optimize for specific actions
70+
"""
71+
queryset = super().get_queryset()
72+
73+
if self.action == "data":
74+
queryset = queryset.only("id", "shared")
75+
76+
return queryset
77+
5478
def get_serializer_class(self):
5579
"""
5680
Get appropriate serializer class

onadata/apps/api/viewsets/organization_profile_viewset.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ class OrganizationProfileViewSet(
6868
List, Retrieve, Update, Create/Register Organizations.
6969
"""
7070

71-
queryset = OrganizationProfile.objects.filter(user__is_active=True)
71+
queryset = OrganizationProfile.objects.filter(user__is_active=True).select_related(
72+
"user", "creator", "created_by"
73+
)
7274
serializer_class = serializer_from_settings()
7375
lookup_field = "user"
7476
permission_classes = [permissions.OrganizationProfilePermissions]

onadata/apps/api/viewsets/project_viewset.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import logging
77

88
from django.core.mail import send_mail
9+
from django.db.models import Prefetch
910
from django.shortcuts import get_object_or_404
1011
from django.utils.translation import gettext as _
1112

@@ -82,6 +83,27 @@ class ProjectViewSet(
8283
Project.objects.filter(deleted_at__isnull=True)
8384
.order_by("-date_created")
8485
.select_related()
86+
.prefetch_related(
87+
Prefetch(
88+
"xform_set",
89+
queryset=XForm.objects.filter(deleted_at__isnull=True)
90+
.only(
91+
"id",
92+
"title",
93+
"id_string",
94+
"is_merged_dataset",
95+
"encrypted",
96+
"last_submission_time",
97+
"project_id",
98+
)
99+
.prefetch_related(
100+
"registration_forms__entity_list",
101+
"follow_up_forms__entity_list",
102+
"metadata_set",
103+
),
104+
to_attr="xforms_prefetch",
105+
)
106+
)
85107
)
86108
serializer_class = ProjectSerializer
87109
lookup_field = "pk"
@@ -106,6 +128,19 @@ def get_serializer_class(self):
106128

107129
return super().get_serializer_class()
108130

131+
def get_serializer_context(self):
132+
"""Pass starred projects to serializer context."""
133+
context = super().get_serializer_context()
134+
if (
135+
self.action == "list"
136+
and self.request
137+
and not self.request.user.is_anonymous
138+
):
139+
context["starred_projects"] = self.request.user.project_stars.values_list(
140+
"pk", flat=True
141+
)
142+
return context
143+
109144
def get_queryset(self):
110145
"""Use 'prepared' prefetched queryset for GET requests."""
111146
if self.request.method.upper() in ["GET", "OPTIONS"]:

onadata/apps/api/viewsets/team_viewset.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""
33
The /teams API endpoint implementation.
44
"""
5+
56
from django.contrib.auth import get_user_model
67
from django.utils.translation import gettext as _
78

@@ -44,7 +45,9 @@ class TeamViewSet(
4445
This endpoint allows you to create, update and view team information.
4546
"""
4647

47-
queryset = Team.objects.all()
48+
queryset = Team.objects.select_related(
49+
"organization", "created_by"
50+
).prefetch_related("user_set", "projects")
4851
serializer_class = TeamSerializer
4952
lookup_field = "pk"
5053
extra_lookup_fields = None

onadata/apps/logger/models/project.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ def get_queryset(self):
3333
# pylint: disable=invalid-name
3434
Team = apps.get_model("api", "Team") # noqa N806
3535
XForm = apps.get_model("logger", "XForm") # noqa N806
36+
EntityList = apps.get_model("logger", "EntityList") # noqa N806
37+
RegistrationForm = apps.get_model("logger", "RegistrationForm") # noqa N806
38+
FollowUpForm = apps.get_model("logger", "FollowUpForm") # noqa N806
3639

3740
# pylint: disable=no-member
3841
return (
@@ -44,24 +47,59 @@ def get_queryset(self):
4447
"xform_set",
4548
queryset=XForm.objects.filter(deleted_at__isnull=True)
4649
.select_related("user")
47-
.prefetch_related("user")
4850
.prefetch_related("dataview_set")
4951
.prefetch_related("metadata_set")
52+
.prefetch_related(
53+
Prefetch(
54+
"registration_forms",
55+
queryset=RegistrationForm.objects.filter(
56+
entity_list__deleted_at__isnull=True
57+
).select_related("entity_list"),
58+
)
59+
)
60+
.prefetch_related(
61+
Prefetch(
62+
"follow_up_forms",
63+
queryset=FollowUpForm.objects.filter(
64+
entity_list__deleted_at__isnull=True
65+
).select_related("entity_list"),
66+
)
67+
)
5068
.only(
5169
"id",
5270
"user",
5371
"project",
5472
"title",
5573
"date_created",
5674
"last_submission_time",
75+
"last_updated_at",
5776
"num_of_submissions",
5877
"downloadable",
5978
"id_string",
6079
"is_merged_dataset",
80+
"encrypted",
6181
),
6282
to_attr="xforms_prefetch",
6383
)
6484
)
85+
.prefetch_related(
86+
Prefetch(
87+
"entity_lists",
88+
queryset=EntityList.objects.filter(deleted_at__isnull=True)
89+
.prefetch_related(
90+
Prefetch(
91+
"registrationform_set",
92+
queryset=RegistrationForm.objects.select_related("xform"),
93+
)
94+
)
95+
.prefetch_related(
96+
Prefetch(
97+
"followupform_set",
98+
queryset=FollowUpForm.objects.select_related("xform"),
99+
)
100+
),
101+
)
102+
)
65103
.prefetch_related("tags")
66104
.prefetch_related(
67105
Prefetch(
@@ -86,6 +124,15 @@ def get_queryset(self):
86124
queryset=Team.objects.all().prefetch_related("user_set"),
87125
)
88126
)
127+
.prefetch_related(
128+
Prefetch(
129+
"dataview_set",
130+
queryset=apps.get_model("logger", "DataView").objects.filter(
131+
deleted_at__isnull=True
132+
),
133+
to_attr="dataview_prefetch",
134+
)
135+
)
89136
)
90137

91138

0 commit comments

Comments
 (0)