dnscms: prefetch and select related when quering future events

This commit is contained in:
2026-05-19 23:46:13 +02:00
parent 154338057d
commit 80b9cdbc33
2 changed files with 78 additions and 2 deletions
+18 -2
View File
@@ -1,7 +1,7 @@
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import Min, Q, UniqueConstraint from django.db.models import Min, Prefetch, Q, UniqueConstraint
from django.utils import timezone from django.utils import timezone
from django.utils.html import mark_safe from django.utils.html import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -45,7 +45,23 @@ class EventIndex(HeadlessMixin, Page):
subpage_types = ["events.EventPage"] subpage_types = ["events.EventPage"]
def future_events(self, info, **kwargs): def future_events(self, info, **kwargs):
return EventPage.objects.live().future().order_by("next_occurrence") return (
EventPage.objects.live()
.future()
.order_by("next_occurrence")
.select_related("featured_image")
.prefetch_related(
Prefetch(
"occurrences",
queryset=EventOccurrence.objects.select_related("venue"),
),
"categories",
Prefetch(
"organizers",
queryset=EventOrganizer.objects.select_related("association"),
),
)
)
graphql_fields = [ graphql_fields = [
GraphQLCollection( GraphQLCollection(
+60
View File
@@ -2,6 +2,8 @@ from datetime import datetime, timedelta
import pytest import pytest
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import connection
from django.test.utils import CaptureQueriesContext
from django.utils import timezone from django.utils import timezone
from events.admin import EventDateColumn, OrganizersColumn from events.admin import EventDateColumn, OrganizersColumn
@@ -188,6 +190,64 @@ def test_graphql_event_index_future_events_query(event_index, graphql_post):
assert "Upcoming gig" in titles assert "Upcoming gig" in titles
def test_future_events_does_not_have_n_plus_one_queries(
event_index, venue, association_index, graphql_post
):
"""Regression test: query count for futureEvents stays bounded as events grow."""
konsert = EventCategory.objects.create(name="Konsert", slug="konsert")
association = AssociationPageFactory(parent=association_index, title="DNS")
org = EventOrganizer.objects.create(name="Forening", slug="forening", association=association)
image = CustomImageFactory(title="Cover")
now = timezone.now()
for i in range(5):
event = EventPageFactory(
parent=event_index,
title=f"Event {i}",
body=[("paragraph", "<p>x</p>")],
featured_image=image,
)
event.categories.add(konsert)
EventOrganizerLink.objects.create(event=event, organizer=org)
EventOccurrence.objects.create(
event=event,
start=now + timedelta(days=i + 1),
venue=venue,
)
home_query = """
query {
eventIndex {
futureEvents {
id
title
subtitle
body { blockType }
featuredImage { url }
occurrences { start end venueCustom venue { title } }
categories { name slug }
organizers { name slug association { title } }
}
}
}
"""
with CaptureQueriesContext(connection) as ctx:
response, body = graphql_post(home_query)
assert response.status_code == 200
assert "errors" not in body, body
assert len(body["data"]["eventIndex"]["futureEvents"]) == 5
# Bump only alongside an intentional resolver change.
max_queries = 6
assert len(ctx) <= max_queries, (
f"futureEvents took {len(ctx)} queries for 5 events — likely N+1. "
f"Captured queries:\n"
+ "\n".join(f" {i + 1}. {q['sql'][:120]}" for i, q in enumerate(ctx.captured_queries))
)
def test_graphql_event_index_future_events_ordered_by_next_occurrence(event_index, graphql_post): def test_graphql_event_index_future_events_ordered_by_next_occurrence(event_index, graphql_post):
now = timezone.now() now = timezone.now()