Use django-auto-prefetch
This commit is contained in:
parent
162d752a22
commit
94752383b1
6 changed files with 298 additions and 128 deletions
|
|
@ -0,0 +1,102 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-12 22:03
|
||||
|
||||
import django.db.models.manager
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('twitch', '0013_dropcampaign_allow_is_enabled_channel_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='channel',
|
||||
options={'base_manager_name': 'prefetch_manager', 'ordering': ['display_name']},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='dropbenefit',
|
||||
options={'base_manager_name': 'prefetch_manager', 'ordering': ['-created_at']},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='dropbenefitedge',
|
||||
options={'base_manager_name': 'prefetch_manager'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='dropcampaign',
|
||||
options={'base_manager_name': 'prefetch_manager', 'ordering': ['-start_at']},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='game',
|
||||
options={'base_manager_name': 'prefetch_manager', 'ordering': ['display_name']},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='notificationsubscription',
|
||||
options={'base_manager_name': 'prefetch_manager'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='organization',
|
||||
options={'base_manager_name': 'prefetch_manager', 'ordering': ['name']},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='timebaseddrop',
|
||||
options={'base_manager_name': 'prefetch_manager', 'ordering': ['start_at']},
|
||||
),
|
||||
migrations.AlterModelManagers(
|
||||
name='channel',
|
||||
managers=[
|
||||
('objects', django.db.models.manager.Manager()),
|
||||
('prefetch_manager', django.db.models.manager.Manager()),
|
||||
],
|
||||
),
|
||||
migrations.AlterModelManagers(
|
||||
name='dropbenefit',
|
||||
managers=[
|
||||
('objects', django.db.models.manager.Manager()),
|
||||
('prefetch_manager', django.db.models.manager.Manager()),
|
||||
],
|
||||
),
|
||||
migrations.AlterModelManagers(
|
||||
name='dropbenefitedge',
|
||||
managers=[
|
||||
('objects', django.db.models.manager.Manager()),
|
||||
('prefetch_manager', django.db.models.manager.Manager()),
|
||||
],
|
||||
),
|
||||
migrations.AlterModelManagers(
|
||||
name='dropcampaign',
|
||||
managers=[
|
||||
('objects', django.db.models.manager.Manager()),
|
||||
('prefetch_manager', django.db.models.manager.Manager()),
|
||||
],
|
||||
),
|
||||
migrations.AlterModelManagers(
|
||||
name='game',
|
||||
managers=[
|
||||
('objects', django.db.models.manager.Manager()),
|
||||
('prefetch_manager', django.db.models.manager.Manager()),
|
||||
],
|
||||
),
|
||||
migrations.AlterModelManagers(
|
||||
name='notificationsubscription',
|
||||
managers=[
|
||||
('objects', django.db.models.manager.Manager()),
|
||||
('prefetch_manager', django.db.models.manager.Manager()),
|
||||
],
|
||||
),
|
||||
migrations.AlterModelManagers(
|
||||
name='organization',
|
||||
managers=[
|
||||
('objects', django.db.models.manager.Manager()),
|
||||
('prefetch_manager', django.db.models.manager.Manager()),
|
||||
],
|
||||
),
|
||||
migrations.AlterModelManagers(
|
||||
name='timebaseddrop',
|
||||
managers=[
|
||||
('objects', django.db.models.manager.Manager()),
|
||||
('prefetch_manager', django.db.models.manager.Manager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-12 22:18
|
||||
|
||||
import auto_prefetch
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('twitch', '0014_alter_channel_options_alter_dropbenefit_options_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='dropbenefitedge',
|
||||
name='benefit',
|
||||
field=auto_prefetch.ForeignKey(help_text='The benefit in this relationship.', on_delete=django.db.models.deletion.CASCADE, to='twitch.dropbenefit'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='dropbenefitedge',
|
||||
name='drop',
|
||||
field=auto_prefetch.ForeignKey(help_text='The time-based drop in this relationship.', on_delete=django.db.models.deletion.CASCADE, to='twitch.timebaseddrop'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='dropcampaign',
|
||||
name='game',
|
||||
field=auto_prefetch.ForeignKey(help_text='Game associated with this campaign.', on_delete=django.db.models.deletion.CASCADE, related_name='drop_campaigns', to='twitch.game', verbose_name='Game'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='game',
|
||||
name='owner',
|
||||
field=auto_prefetch.ForeignKey(blank=True, help_text='The organization that owns this game.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='games', to='twitch.organization', verbose_name='Organization'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notificationsubscription',
|
||||
name='game',
|
||||
field=auto_prefetch.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='twitch.game'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notificationsubscription',
|
||||
name='organization',
|
||||
field=auto_prefetch.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='twitch.organization'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notificationsubscription',
|
||||
name='user',
|
||||
field=auto_prefetch.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='timebaseddrop',
|
||||
name='campaign',
|
||||
field=auto_prefetch.ForeignKey(help_text='The campaign this drop belongs to.', on_delete=django.db.models.deletion.CASCADE, related_name='time_based_drops', to='twitch.dropcampaign'),
|
||||
),
|
||||
]
|
||||
|
|
@ -5,6 +5,7 @@ import re
|
|||
from typing import TYPE_CHECKING, ClassVar
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
import auto_prefetch
|
||||
from django.contrib.postgres.indexes import GinIndex
|
||||
from django.contrib.postgres.search import SearchVectorField
|
||||
from django.db import models
|
||||
|
|
@ -18,7 +19,7 @@ if TYPE_CHECKING:
|
|||
logger: logging.Logger = logging.getLogger("ttvdrops")
|
||||
|
||||
|
||||
class Organization(models.Model):
|
||||
class Organization(auto_prefetch.Model):
|
||||
"""Represents an organization on Twitch that can own drop campaigns."""
|
||||
|
||||
id = models.CharField(
|
||||
|
|
@ -48,7 +49,7 @@ class Organization(models.Model):
|
|||
# PostgreSQL full-text search field
|
||||
search_vector = SearchVectorField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
ordering = ["name"]
|
||||
indexes: ClassVar[list] = [
|
||||
# Regular B-tree index for name lookups
|
||||
|
|
@ -62,7 +63,7 @@ class Organization(models.Model):
|
|||
return self.name or self.id
|
||||
|
||||
|
||||
class Game(models.Model):
|
||||
class Game(auto_prefetch.Model):
|
||||
"""Represents a game on Twitch."""
|
||||
|
||||
id = models.CharField(max_length=255, primary_key=True, verbose_name="Game ID")
|
||||
|
|
@ -98,7 +99,7 @@ class Game(models.Model):
|
|||
# PostgreSQL full-text search field
|
||||
search_vector = SearchVectorField(null=True, blank=True)
|
||||
|
||||
owner = models.ForeignKey(
|
||||
owner = auto_prefetch.ForeignKey(
|
||||
Organization,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="games",
|
||||
|
|
@ -118,7 +119,7 @@ class Game(models.Model):
|
|||
help_text="Timestamp when this game record was last updated.",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
ordering = ["display_name"]
|
||||
indexes: ClassVar[list] = [
|
||||
models.Index(fields=["slug"]),
|
||||
|
|
@ -180,7 +181,7 @@ class Game(models.Model):
|
|||
return ""
|
||||
|
||||
|
||||
class Channel(models.Model):
|
||||
class Channel(auto_prefetch.Model):
|
||||
"""Represents a Twitch channel that can participate in drop campaigns."""
|
||||
|
||||
id = models.CharField(
|
||||
|
|
@ -212,7 +213,7 @@ class Channel(models.Model):
|
|||
help_text="Timestamp when this channel record was last updated.",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
ordering = ["display_name"]
|
||||
indexes: ClassVar[list] = [
|
||||
models.Index(fields=["name"]),
|
||||
|
|
@ -224,7 +225,7 @@ class Channel(models.Model):
|
|||
return self.display_name or self.name or self.id
|
||||
|
||||
|
||||
class DropCampaign(models.Model):
|
||||
class DropCampaign(auto_prefetch.Model):
|
||||
"""Represents a Twitch drop campaign."""
|
||||
|
||||
id = models.CharField(
|
||||
|
|
@ -289,7 +290,7 @@ class DropCampaign(models.Model):
|
|||
# PostgreSQL full-text search field
|
||||
search_vector = SearchVectorField(null=True, blank=True)
|
||||
|
||||
game = models.ForeignKey(
|
||||
game = auto_prefetch.ForeignKey(
|
||||
Game,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="drop_campaigns",
|
||||
|
|
@ -307,7 +308,7 @@ class DropCampaign(models.Model):
|
|||
help_text="Timestamp when this campaign record was last updated.",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
ordering = ["-start_at"]
|
||||
constraints = [
|
||||
# Ensure end_at is after start_at when both are set
|
||||
|
|
@ -368,7 +369,7 @@ class DropCampaign(models.Model):
|
|||
return self.name
|
||||
|
||||
|
||||
class DropBenefit(models.Model):
|
||||
class DropBenefit(auto_prefetch.Model):
|
||||
"""Represents a benefit that can be earned from a drop."""
|
||||
|
||||
id = models.CharField(
|
||||
|
|
@ -425,7 +426,7 @@ class DropBenefit(models.Model):
|
|||
help_text="Timestamp when this benefit record was last updated.",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
ordering = ["-created_at"]
|
||||
indexes: ClassVar[list] = [
|
||||
# Regular B-tree index for benefit name lookups
|
||||
|
|
@ -443,7 +444,7 @@ class DropBenefit(models.Model):
|
|||
return self.name
|
||||
|
||||
|
||||
class TimeBasedDrop(models.Model):
|
||||
class TimeBasedDrop(auto_prefetch.Model):
|
||||
"""Represents a time-based drop in a drop campaign."""
|
||||
|
||||
id = models.CharField(
|
||||
|
|
@ -483,7 +484,7 @@ class TimeBasedDrop(models.Model):
|
|||
search_vector = SearchVectorField(null=True, blank=True)
|
||||
|
||||
# Foreign keys
|
||||
campaign = models.ForeignKey(
|
||||
campaign = auto_prefetch.ForeignKey(
|
||||
DropCampaign,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="time_based_drops",
|
||||
|
|
@ -506,7 +507,7 @@ class TimeBasedDrop(models.Model):
|
|||
help_text="Timestamp when this time-based drop record was last updated.",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
ordering = ["start_at"]
|
||||
constraints = [
|
||||
# Ensure end_at is after start_at when both are set
|
||||
|
|
@ -535,15 +536,15 @@ class TimeBasedDrop(models.Model):
|
|||
return self.name
|
||||
|
||||
|
||||
class DropBenefitEdge(models.Model):
|
||||
class DropBenefitEdge(auto_prefetch.Model):
|
||||
"""Represents the relationship between a TimeBasedDrop and a DropBenefit."""
|
||||
|
||||
drop = models.ForeignKey(
|
||||
drop = auto_prefetch.ForeignKey(
|
||||
TimeBasedDrop,
|
||||
on_delete=models.CASCADE,
|
||||
help_text="The time-based drop in this relationship.",
|
||||
)
|
||||
benefit = models.ForeignKey(
|
||||
benefit = auto_prefetch.ForeignKey(
|
||||
DropBenefit,
|
||||
on_delete=models.CASCADE,
|
||||
help_text="The benefit in this relationship.",
|
||||
|
|
@ -563,7 +564,7 @@ class DropBenefitEdge(models.Model):
|
|||
help_text="Timestamp when this drop-benefit edge was last updated.",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=("drop", "benefit"), name="unique_drop_benefit"),
|
||||
]
|
||||
|
|
@ -576,12 +577,12 @@ class DropBenefitEdge(models.Model):
|
|||
return f"{self.drop.name} - {self.benefit.name}"
|
||||
|
||||
|
||||
class NotificationSubscription(models.Model):
|
||||
class NotificationSubscription(auto_prefetch.Model):
|
||||
"""Users can subscribe to games to get notified."""
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
game = models.ForeignKey(Game, null=True, blank=True, on_delete=models.CASCADE)
|
||||
organization = models.ForeignKey(Organization, null=True, blank=True, on_delete=models.CASCADE)
|
||||
user = auto_prefetch.ForeignKey(User, on_delete=models.CASCADE)
|
||||
game = auto_prefetch.ForeignKey(Game, null=True, blank=True, on_delete=models.CASCADE)
|
||||
organization = auto_prefetch.ForeignKey(Organization, null=True, blank=True, on_delete=models.CASCADE)
|
||||
|
||||
notify_found = models.BooleanField(default=False)
|
||||
notify_live = models.BooleanField(default=False)
|
||||
|
|
@ -589,7 +590,7 @@ class NotificationSubscription(models.Model):
|
|||
added_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
unique_together: ClassVar[list[tuple[str, str]]] = [
|
||||
("user", "game"),
|
||||
("user", "organization"),
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ class DropCampaignDetailView(DetailView):
|
|||
if queryset is None:
|
||||
queryset = self.get_queryset()
|
||||
|
||||
queryset = queryset.select_related("game__owner").prefetch_related("allow_channels")
|
||||
queryset = queryset.select_related("game__owner")
|
||||
|
||||
return super().get_object(queryset=queryset)
|
||||
|
||||
|
|
@ -530,10 +530,6 @@ def dashboard(request: HttpRequest) -> HttpResponse:
|
|||
DropCampaign.objects.filter(start_at__lte=now, end_at__gte=now)
|
||||
.select_related("game__owner")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"time_based_drops",
|
||||
queryset=TimeBasedDrop.objects.prefetch_related("benefits"),
|
||||
),
|
||||
"allow_channels",
|
||||
)
|
||||
)
|
||||
|
|
@ -598,19 +594,8 @@ def debug_view(request: HttpRequest) -> HttpResponse:
|
|||
).select_related("game")
|
||||
|
||||
# Benefits with missing images
|
||||
broken_benefit_images: QuerySet[DropBenefit] = (
|
||||
DropBenefit.objects.annotate(
|
||||
trimmed_url=Trim("image_asset_url") # Create a temporary field with no whitespace
|
||||
)
|
||||
.filter(
|
||||
Q(image_asset_url__isnull=True)
|
||||
| Q(trimmed_url__exact="") # Check the trimmed URL
|
||||
| ~Q(image_asset_url__startswith="http")
|
||||
)
|
||||
.prefetch_related(
|
||||
# Prefetch the path to the game to avoid N+1 queries in the template
|
||||
Prefetch("drops", queryset=TimeBasedDrop.objects.select_related("campaign__game"))
|
||||
)
|
||||
broken_benefit_images: QuerySet[DropBenefit] = DropBenefit.objects.annotate(trimmed_url=Trim("image_asset_url")).filter(
|
||||
Q(image_asset_url__isnull=True) | Q(trimmed_url__exact="") | ~Q(image_asset_url__startswith="http")
|
||||
)
|
||||
|
||||
# Time-based drops without any benefits
|
||||
|
|
@ -833,7 +818,10 @@ class ChannelDetailView(DetailView):
|
|||
.select_related("game__owner")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"time_based_drops", queryset=TimeBasedDrop.objects.prefetch_related(Prefetch("benefits", queryset=DropBenefit.objects.order_by("name")))
|
||||
"time_based_drops",
|
||||
queryset=TimeBasedDrop.objects.prefetch_related(
|
||||
Prefetch("benefits", queryset=DropBenefit.objects.order_by("name")),
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by("-start_at")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue