Решение не разделять логику в django для решения N+ 1 запроса

Вот некоторые из моих моделей:

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)

Чтобы исправить N+1 запрос, мне нужно извлечь order_by из представления с помощью Prefetch:

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

и удалите order_by в моем методе correct_date.

Проблема здесь в том, что значение correct_date зависит от order_by, чтобы получить последнюю дату. Но order_by находится в другом файле. Это может привести к проблемам у тех, кто использует код после.

Есть ли какие-либо другие решения, кроме:

  • Сохраняя код таким, какой он есть, с комментарием #, необходимо использовать order_by('дата') перед
  • Использование сервиса для обработки всей этой логики
  • Проверяем в методе correct_date, вызывался ли он ранее с order_by, и выдаем ошибку или применяем order_by, если это не так

Чтобы избежать проблемы с запросом N+1, сохраняя при этом четкое разделение логики, вы можете сделать свой метод correct_date() более гибким, разрешив ему принимать список предварительно выбранных упорядоченных дат. Это позволяет избежать скрытых зависимостей от order_by() и делает метод вашей модели пригодным для повторного использования и тестирования.

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 с to_attr, чтобы присвоить упорядоченным датам временный атрибут:

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"
    )
)

Использование в представлении или логике шаблона:

Затем передайте предварительно выбранные данные явно методу:

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

Решением, которое я нашел, было создание миксера:

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()

И использовать его без 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]

Таким образом, мне нужно обработать миксинг:

  • Выполнить предварительную выборку в to_attr, если она предусмотрена
  • Явные потребности в предварительной выборке данных при просмотре кода для fct параметра
  • Значение по умолчанию на случай, если разработчик забыл использовать Prefetch
Вернуться на верх