Skip to content

Commit f4cec7f

Browse files
authored
Fix confirmed Django N+1 queries (#4782)
1 parent 8f5c7a3 commit f4cec7f

12 files changed

Lines changed: 107 additions & 31 deletions

File tree

backend/conferences/admin/conference.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,9 @@ def map_videos(self, request, object_id):
283283
return render(request, "admin/videos_upload/map_videos.html", context)
284284

285285
def save_manual_changes(self, request, object_id, data):
286-
all_events = ScheduleItem.objects.filter(conference_id=object_id)
286+
all_events = ScheduleItem.objects.select_related(
287+
"submission", "keynote", "language"
288+
).filter(conference_id=object_id)
287289

288290
for event in all_events:
289291
key_name = f"video_uploaded_path_{event.id}"
@@ -303,9 +305,16 @@ def save_manual_changes(self, request, object_id, data):
303305

304306
def run_video_uploaded_path_matcher(self, request, object_id, ignore_cache):
305307
conference = Conference.objects.get(pk=object_id)
306-
all_events = conference.schedule_items.prefetch_related(
307-
"submission", "additional_speakers"
308-
).all()
308+
all_events = (
309+
conference.schedule_items.select_related(
310+
"submission__speaker", "keynote", "language"
311+
)
312+
.prefetch_related(
313+
"additional_speakers__user",
314+
"keynote__speakers__user",
315+
)
316+
.all()
317+
)
309318

310319
cache_key = f"{conference.code}:video-upload-files-cache"
311320
files = cache.get(cache_key)

backend/grants/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,7 @@ def get_queryset(self, request):
760760
qs = (
761761
super()
762762
.get_queryset(request)
763-
.select_related("user")
763+
.select_related("user", "conference")
764764
.prefetch_related("reimbursements__category")
765765
.annotate(
766766
is_proposed_speaker=Exists(

backend/grants/models.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from decimal import Decimal
22

33
from django.db import models
4+
from django.db.models import prefetch_related_objects
45
from django.urls import reverse
56
from django.utils.translation import gettext_lazy as _
67
from model_utils.models import TimeStampedModel
@@ -307,10 +308,12 @@ def total_allocated_amount(self) -> Decimal:
307308
@property
308309
def total_grantee_reimbursement_amount(self) -> Decimal:
309310
"""Return total reimbursement excluding ticket."""
311+
reimbursements = list(self.reimbursements.all())
312+
prefetch_related_objects(reimbursements, "category")
310313
return sum(
311314
(
312315
r.granted_amount
313-
for r in self.reimbursements.all()
316+
for r in reimbursements
314317
if r.category.category != GrantReimbursementCategory.Category.TICKET
315318
),
316319
start=Decimal(0),

backend/grants/tasks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def create_and_send_voucher_to_grantee(*, grant_id: int) -> None:
7878
@app.task
7979
def send_grant_reply_approved_email(*, grant_id: int, is_reminder: bool) -> None:
8080
logger.info("Sending Reply APPROVED email for Grant %s", grant_id)
81-
grant = Grant.objects.get(id=grant_id)
81+
grant = Grant.objects.prefetch_related("reimbursements__category").get(id=grant_id)
8282

8383
total_amount = grant.total_grantee_reimbursement_amount
8484
ticket_only = grant.has_ticket_only()

backend/grants/tests/test_models.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,28 @@ def test_total_grantee_reimbursement_amount_excludes_ticket():
254254
assert grant.total_grantee_reimbursement_amount == Decimal("700")
255255

256256

257+
def test_total_grantee_reimbursement_amount_batches_categories(
258+
django_assert_num_queries,
259+
):
260+
grant = GrantFactory()
261+
GrantReimbursementFactory(
262+
grant=grant,
263+
category__conference=grant.conference,
264+
category__travel=True,
265+
granted_amount=Decimal("500"),
266+
)
267+
GrantReimbursementFactory(
268+
grant=grant,
269+
category__conference=grant.conference,
270+
category__accommodation=True,
271+
granted_amount=Decimal("200"),
272+
)
273+
grant = Grant.objects.get(pk=grant.pk)
274+
275+
with django_assert_num_queries(2):
276+
assert grant.total_grantee_reimbursement_amount == Decimal("700")
277+
278+
257279
def test_total_grantee_reimbursement_amount_with_only_ticket():
258280
grant = GrantFactory()
259281
GrantReimbursementFactory(

backend/integrations/plain_cards.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
def create_grant_card(request, user, conference):
66
grant = (
7-
Grant.objects.of_user(user).for_conference(conference).first()
7+
Grant.objects.of_user(user)
8+
.for_conference(conference)
9+
.prefetch_related("reimbursements__category")
10+
.first()
811
if user
912
else None
1013
)

backend/reviews/adapters.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,16 @@ def process_shortlist_post(
506506
if key.startswith("notes-")
507507
}
508508

509-
grants = list(review_session.conference.grants.filter(id__in=decisions.keys()))
509+
grants = list(
510+
review_session.conference.grants.filter(
511+
id__in=decisions.keys()
512+
).prefetch_related(
513+
Prefetch(
514+
"reimbursements",
515+
queryset=GrantReimbursement.objects.select_related("category"),
516+
)
517+
)
518+
)
510519

511520
# Track grants with pending status changes for audit logging
512521
grants_with_pending_status_changes = {}
@@ -526,13 +535,14 @@ def process_shortlist_post(
526535
grants_with_pending_status_changes[grant.id] = original_pending_status
527536

528537
# Handle reimbursement deletions
529-
if grant.reimbursements.exists():
538+
reimbursements = list(grant.reimbursements.all())
539+
if reimbursements:
530540
approved_reimbursement_categories = (
531541
approved_reimbursement_categories_decisions.get(grant.id, [])
532542
)
533543
if decision != Grant.Status.approved:
534544
# Delete all reimbursements if not approved
535-
for reimbursement in grant.reimbursements.all():
545+
for reimbursement in reimbursements:
536546
create_deletion_admin_log_entry(
537547
request.user,
538548
grant,
@@ -541,16 +551,21 @@ def process_shortlist_post(
541551
reimbursement.delete()
542552
else:
543553
# Only keep those in current approved categories
544-
to_delete = grant.reimbursements.exclude(
545-
category_id__in=approved_reimbursement_categories
546-
)
554+
to_delete = [
555+
reimbursement
556+
for reimbursement in reimbursements
557+
if reimbursement.category_id
558+
not in approved_reimbursement_categories
559+
]
547560
for reimbursement in to_delete:
548561
create_deletion_admin_log_entry(
549562
request.user,
550563
grant,
551564
change_message=f"[Review Session] Reimbursement removed: {reimbursement.category.name}.",
552565
)
553-
to_delete.delete()
566+
GrantReimbursement.objects.filter(
567+
id__in=[reimbursement.id for reimbursement in to_delete]
568+
).delete()
554569

555570
# Save grants and create audit logs
556571
for grant in grants:

backend/schedule/admin.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,12 @@ def get_queryset(self, request):
404404
return (
405405
super()
406406
.get_queryset(request)
407-
.prefetch_related("rooms")
407+
.select_related("submission__speaker")
408+
.prefetch_related(
409+
"rooms",
410+
"additional_speakers__user",
411+
"keynote__speakers__user",
412+
)
408413
.annotate(attendees_count_annotation=Count("attendees", distinct=True))
409414
)
410415

backend/schedule/models.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from django.core import exceptions
77
from django.db import models
8-
from django.db.models import Case, When
8+
from django.db.models import Case, When, prefetch_related_objects
99
from django.urls import reverse
1010
from django.utils.functional import cached_property
1111
from django.utils.translation import gettext_lazy as _
@@ -341,11 +341,18 @@ def speakers(self):
341341
speakers.append(self.submission.speaker)
342342

343343
if self.keynote_id:
344-
for speaker_keynote in self.keynote.speakers.order_by("id").all():
344+
keynote_speakers = list(self.keynote.speakers.all())
345+
prefetch_related_objects(keynote_speakers, "user")
346+
for speaker_keynote in sorted(
347+
keynote_speakers, key=lambda speaker: speaker.id
348+
):
345349
speakers.append(speaker_keynote.user)
346350

351+
additional_speakers = list(self.additional_speakers.all())
352+
prefetch_related_objects(additional_speakers, "user")
347353
speakers.extend(
348-
[speaker.user for speaker in self.additional_speakers.order_by("id").all()]
354+
speaker.user
355+
for speaker in sorted(additional_speakers, key=lambda speaker: speaker.id)
349356
)
350357
return [speaker for speaker in speakers if speaker is not None]
351358

backend/schedule/tasks.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ def process_schedule_items_videos_to_upload():
345345
last_attempt_at__lt=timezone.now() - timezone.timedelta(hours=1),
346346
)
347347
)
348+
.select_related("schedule_item")
348349
.to_upload()
349350
.order_by("last_attempt_at")
350351
)

0 commit comments

Comments
 (0)