Solution to not split logic in django to solve N+1 queries

Here are some of my models:

class CustomUser(AbstractUser):
    def correct_date(self, date=None):
        res = self.dates.order_by("date").all()
        if not len(res):
            return None
        return res[len(res) - 1]


class Date(models.Model):
    user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name="dates")
    date = models.DateField(auto_now_add=True)

To fix N+1 queries I need to extract the order_by in the view with a Prefetch:

queryset = Project.objects.prefetch_related(
        Prefetch(
            "user__dates",
            queryset=Date.objects.order_by('date')
        ),
)

and remove the order_by in my correct_date method.

The problem here is that correct_date depends on the order_by in order to retrieve the last date. But the order_by is in an other file. That could leeds to issues for the ones using the code after.

Is there any other solutions than:

  • Keeping the code as it is with a comment # needs to be used with an order_by('date') before
  • Using a service to handle all that logic
  • Checking in the correct_date method if it was called with an order_by before, and throwing an error or apply order_by if it wasn't

To avoid the N+1 query issue while maintaining clean separation of logic, you can make your correct_date() method more flexible by allowing it to accept a list of pre-fetched, ordered dates. This avoids hidden dependencies on order_by() and makes your model method reusable and testable.

class CustomUser(AbstractUser):
    def correct_date(self, prefetched_dates=None):
        dates = prefetched_dates if prefetched_dates is not None else self.dates.order_by("date").all()
        return dates.last() if dates else None

Prefetch in the View:

Use Prefetch with to_attr to assign the ordered dates to a temporary attribute:

from django.db.models import Prefetch

queryset = Project.objects.prefetch_related(
    Prefetch(
        "user__dates",
        queryset=Date.objects.order_by("date"),
        to_attr="prefetched_ordered_dates"
    )
)

Usage in View or Template Logic:

Then pass the prefetched data explicitly to the method:

for project in queryset:
    user = project.user
    last_date = user.correct_date(getattr(user, "prefetched_ordered_dates", None))

The solution I found was creating a Mixin:

class PrefetchedDataMixin:
    def has_prefetch(self, to_attr: str = "", related_name: str = ""):
        return (
            hasattr(self, '_prefetched_objects_cache') and related_name in self._prefetched_objects_cache
            if related_name
            else hasattr(self, to_attr))

    def get_prefetch(self, fct, to_attr: str = "", related_name: str = ""):
        if self.has_prefetch(to_attr=to_attr, related_name=related_name):
            return getattr(self, to_attr if to_attr else related_name)
        return fct()

And use it un CustomUser:

class CustomUser(AbstractUser, PrefetchedDataMixin):
    def correct_date(self, date=None):
        res = self.get_prefetch(
            lambda: self.dates.order_by('date'),
            related_name="dates"
        ).all()
        if not len(res):
            return None
        return res[len(res) - 1]

That way I have a Mixin to handle:

  • Prefetch to a to_attr if provided
  • Explicit needs of prefetch data when looking at code for fct param
  • A default value in case devlopper forgot to use Prefetch
Вернуться на верх