Решение не разделять логику в 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