Enhance performance by prefetching more

This commit is contained in:
Joakim Hellsén 2026-04-10 22:32:38 +02:00
commit fb087a01c0
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
6 changed files with 104 additions and 12 deletions

View file

@ -1,7 +1,9 @@
from datetime import timedelta
from typing import TYPE_CHECKING
from django.db import connection
from django.test import TestCase
from django.test.utils import CaptureQueriesContext
from django.urls import reverse
from django.utils import timezone
@ -16,6 +18,75 @@ if TYPE_CHECKING:
class ChzzkDashboardViewTests(TestCase):
"""Test cases for the dashboard view of the chzzk app."""
def test_dashboard_view_no_n_plus_one_on_rewards(self) -> None:
"""Test that the dashboard view does not trigger an N+1 query for rewards."""
now = timezone.now()
base_kwargs = {
"category_type": "game",
"category_id": "1",
"category_value": "TestGame",
"service_id": "chzzk",
"state": "ACTIVE",
"start_date": now - timedelta(days=1),
"end_date": now + timedelta(days=1),
"has_ios_based_reward": False,
"drops_campaign_not_started": False,
"source_api": "unit-test",
}
reward_kwargs = {
"reward_type": "ITEM",
"campaign_reward_type": "Standard",
"condition_type": "watch",
"ios_based_reward": False,
"code_remaining_count": 100,
}
campaign1 = ChzzkCampaign.objects.create(
campaign_no=9001,
title="C1",
**base_kwargs,
)
campaign1.rewards.create( # pyright: ignore[reportAttributeAccessIssue]
reward_no=901,
title="R1",
condition_for_minutes=10,
**reward_kwargs,
) # pyright: ignore[reportAttributeAccessIssue]
campaign2 = ChzzkCampaign.objects.create(
campaign_no=9002,
title="C2",
**base_kwargs,
)
campaign2.rewards.create( # pyright: ignore[reportAttributeAccessIssue]
reward_no=902,
title="R2",
condition_for_minutes=20,
**reward_kwargs,
) # pyright: ignore[reportAttributeAccessIssue]
with CaptureQueriesContext(connection) as one_campaign_ctx:
self.client.get(reverse("chzzk:dashboard"))
query_count_two = len(one_campaign_ctx)
campaign3 = ChzzkCampaign.objects.create(
campaign_no=9003,
title="C3",
**base_kwargs,
)
campaign3.rewards.create( # pyright: ignore[reportAttributeAccessIssue]
reward_no=903,
title="R3",
condition_for_minutes=30,
**reward_kwargs,
) # pyright: ignore[reportAttributeAccessIssue]
with CaptureQueriesContext(connection) as three_campaign_ctx:
self.client.get(reverse("chzzk:dashboard"))
query_count_three = len(three_campaign_ctx)
# With prefetch_related, adding more campaigns should not add extra queries per campaign.
assert query_count_two == query_count_three
def test_dashboard_view_excludes_testing_state_campaigns(self) -> None:
"""Test that the dashboard view excludes campaigns in the TESTING state."""
now: datetime = timezone.now()

View file

@ -34,6 +34,7 @@ def dashboard_view(request: HttpRequest) -> HttpResponse:
models.ChzzkCampaign.objects
.filter(end_date__gte=timezone.now())
.exclude(state="TESTING")
.prefetch_related("rewards")
.order_by("-start_date")
)
return render(

View file

@ -224,6 +224,10 @@ DATABASES: dict[str, dict[str, Any]] = configure_databases(
base_dir=BASE_DIR,
)
if DEBUG:
INSTALLED_APPS.append("zeal")
MIDDLEWARE.append("zeal.middleware.zeal_middleware")
if not TESTING:
INSTALLED_APPS = [*INSTALLED_APPS, "debug_toolbar", "silk"]
MIDDLEWARE = [

View file

@ -292,10 +292,19 @@ class KickDropCampaign(auto_prefetch.Model):
def image_url(self) -> str:
"""Return the image URL for the campaign."""
# Image from first drop
if self.rewards.exists(): # pyright: ignore[reportAttributeAccessIssue]
first_reward: KickReward | None = self.rewards.first() # pyright: ignore[reportAttributeAccessIssue]
if first_reward and first_reward.image_url:
return first_reward.full_image_url
rewards_prefetched: list[KickReward] | None = getattr(
self,
"rewards_ordered",
None,
)
if rewards_prefetched is not None:
first_reward: KickReward | None = (
rewards_prefetched[0] if rewards_prefetched else None
)
else:
first_reward = self.rewards.first() # pyright: ignore[reportAttributeAccessIssue]
if first_reward and first_reward.image_url:
return first_reward.full_image_url
if self.category and self.category.image_url:
return self.category.image_url

View file

@ -31,6 +31,7 @@ dependencies = [
"setproctitle",
"sitemap-parser",
"tqdm",
"django-zeal>=2.1.0",
]

View file

@ -438,7 +438,13 @@ def drop_campaign_list_view(request: HttpRequest) -> HttpResponse: # noqa: PLR0
if game_filter:
queryset = queryset.filter(game__twitch_id=game_filter)
queryset = queryset.prefetch_related("game__owners").order_by("-start_at")
queryset = queryset.prefetch_related(
"game__owners",
Prefetch(
"time_based_drops",
queryset=TimeBasedDrop.objects.prefetch_related("benefits"),
),
).order_by("-start_at")
# Optionally filter by status (active, upcoming, expired)
now: datetime.datetime = timezone.now()
@ -588,18 +594,18 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo
queryset=Channel.objects.order_by("display_name"),
to_attr="channels_ordered",
),
Prefetch(
"time_based_drops",
queryset=TimeBasedDrop.objects.prefetch_related("benefits").order_by(
"required_minutes_watched",
),
),
).get(twitch_id=twitch_id)
except DropCampaign.DoesNotExist as exc:
msg = "No campaign found matching the query"
raise Http404(msg) from exc
drops: QuerySet[TimeBasedDrop] = (
TimeBasedDrop.objects
.filter(campaign=campaign)
.select_related("campaign")
.prefetch_related("benefits")
.order_by("required_minutes_watched")
)
drops: QuerySet[TimeBasedDrop] = campaign.time_based_drops.all() # pyright: ignore[reportAttributeAccessIssue]
now: datetime.datetime = timezone.now()
enhanced_drops: list[dict[str, Any]] = _enhance_drops_with_context(drops, now)