Enhance performance by prefetching more
This commit is contained in:
parent
66ea46cf23
commit
fb087a01c0
6 changed files with 104 additions and 12 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -292,8 +292,17 @@ 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]
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ dependencies = [
|
|||
"setproctitle",
|
||||
"sitemap-parser",
|
||||
"tqdm",
|
||||
"django-zeal>=2.1.0",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue