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 datetime import timedelta
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from django.db import connection
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.test.utils import CaptureQueriesContext
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
@ -16,6 +18,75 @@ if TYPE_CHECKING:
|
||||||
class ChzzkDashboardViewTests(TestCase):
|
class ChzzkDashboardViewTests(TestCase):
|
||||||
"""Test cases for the dashboard view of the chzzk app."""
|
"""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:
|
def test_dashboard_view_excludes_testing_state_campaigns(self) -> None:
|
||||||
"""Test that the dashboard view excludes campaigns in the TESTING state."""
|
"""Test that the dashboard view excludes campaigns in the TESTING state."""
|
||||||
now: datetime = timezone.now()
|
now: datetime = timezone.now()
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ def dashboard_view(request: HttpRequest) -> HttpResponse:
|
||||||
models.ChzzkCampaign.objects
|
models.ChzzkCampaign.objects
|
||||||
.filter(end_date__gte=timezone.now())
|
.filter(end_date__gte=timezone.now())
|
||||||
.exclude(state="TESTING")
|
.exclude(state="TESTING")
|
||||||
|
.prefetch_related("rewards")
|
||||||
.order_by("-start_date")
|
.order_by("-start_date")
|
||||||
)
|
)
|
||||||
return render(
|
return render(
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,10 @@ DATABASES: dict[str, dict[str, Any]] = configure_databases(
|
||||||
base_dir=BASE_DIR,
|
base_dir=BASE_DIR,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
INSTALLED_APPS.append("zeal")
|
||||||
|
MIDDLEWARE.append("zeal.middleware.zeal_middleware")
|
||||||
|
|
||||||
if not TESTING:
|
if not TESTING:
|
||||||
INSTALLED_APPS = [*INSTALLED_APPS, "debug_toolbar", "silk"]
|
INSTALLED_APPS = [*INSTALLED_APPS, "debug_toolbar", "silk"]
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|
|
||||||
|
|
@ -292,8 +292,17 @@ class KickDropCampaign(auto_prefetch.Model):
|
||||||
def image_url(self) -> str:
|
def image_url(self) -> str:
|
||||||
"""Return the image URL for the campaign."""
|
"""Return the image URL for the campaign."""
|
||||||
# Image from first drop
|
# Image from first drop
|
||||||
if self.rewards.exists(): # pyright: ignore[reportAttributeAccessIssue]
|
rewards_prefetched: list[KickReward] | None = getattr(
|
||||||
first_reward: KickReward | None = self.rewards.first() # pyright: ignore[reportAttributeAccessIssue]
|
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:
|
if first_reward and first_reward.image_url:
|
||||||
return first_reward.full_image_url
|
return first_reward.full_image_url
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ dependencies = [
|
||||||
"setproctitle",
|
"setproctitle",
|
||||||
"sitemap-parser",
|
"sitemap-parser",
|
||||||
"tqdm",
|
"tqdm",
|
||||||
|
"django-zeal>=2.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -438,7 +438,13 @@ def drop_campaign_list_view(request: HttpRequest) -> HttpResponse: # noqa: PLR0
|
||||||
if game_filter:
|
if game_filter:
|
||||||
queryset = queryset.filter(game__twitch_id=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)
|
# Optionally filter by status (active, upcoming, expired)
|
||||||
now: datetime.datetime = timezone.now()
|
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"),
|
queryset=Channel.objects.order_by("display_name"),
|
||||||
to_attr="channels_ordered",
|
to_attr="channels_ordered",
|
||||||
),
|
),
|
||||||
|
Prefetch(
|
||||||
|
"time_based_drops",
|
||||||
|
queryset=TimeBasedDrop.objects.prefetch_related("benefits").order_by(
|
||||||
|
"required_minutes_watched",
|
||||||
|
),
|
||||||
|
),
|
||||||
).get(twitch_id=twitch_id)
|
).get(twitch_id=twitch_id)
|
||||||
except DropCampaign.DoesNotExist as exc:
|
except DropCampaign.DoesNotExist as exc:
|
||||||
msg = "No campaign found matching the query"
|
msg = "No campaign found matching the query"
|
||||||
raise Http404(msg) from exc
|
raise Http404(msg) from exc
|
||||||
|
|
||||||
drops: QuerySet[TimeBasedDrop] = (
|
drops: QuerySet[TimeBasedDrop] = campaign.time_based_drops.all() # pyright: ignore[reportAttributeAccessIssue]
|
||||||
TimeBasedDrop.objects
|
|
||||||
.filter(campaign=campaign)
|
|
||||||
.select_related("campaign")
|
|
||||||
.prefetch_related("benefits")
|
|
||||||
.order_by("required_minutes_watched")
|
|
||||||
)
|
|
||||||
|
|
||||||
now: datetime.datetime = timezone.now()
|
now: datetime.datetime = timezone.now()
|
||||||
enhanced_drops: list[dict[str, Any]] = _enhance_drops_with_context(drops, now)
|
enhanced_drops: list[dict[str, Any]] = _enhance_drops_with_context(drops, now)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue