#python #django #django-orm
#python #django #django-orm
Вопрос:
Существуют модели и отношения тезисов :
Hours --FK--> Task --FK--> Project <--FK-- Period
class Hour(models.Model):
date = models.DateField(...)
task = models.ForeignKey(Task, ...)
class Task(models.Model):
project = models.ForeignKey(Project, ...)
class Project(models.Model):
pass
class Period(models.Model):
project = models.ForeignKey(Project,...)
start = models.DateField(...)
end = models.DateField(...)
Summary :
Hour has one task
Task has one project
Period has one project
Hour has a date
Period has a start date and a end date
Для данной даты и данного проекта возможен один или ни один период
Я хочу заполнить period
поле в Hour
objects так же, как это было бы сделано с prefetch_related
помощью (с помощью queryset)
Я хочу иметь что-то вроде этого :
hours = Hour.objects.prefetch_period().all()
hours.first().period # Period(...)
Используя пользовательский метод набора запросов, подобный этому :
class HourQuerySet(models.query.QuerySet):
def prefetch_related(self):
return ???
На данный момент мне удалось сделать это только с помощью annotate
and Subquery
, но мне удается получить только period_id, а не предварительно выбранный период :
def inject_period(self):
period_qs = (
Period.objects.filter(
project__tasks=OuterRef("task"), start__lte=OuterRef("date"), end__gte=OuterRef("date")
)
.values("id")[:1]
)
return self.annotate(period_id=Subquery(period_qs))
Ответ №1:
Я думаю, что единственным способом сделать это в настоящее время было бы определить фильтр для набора Prefetch
запросов объекта.
from django.db.models import Prefetch
Hour.objects.prefetch_related(
Prefetch(
'task__project__periods',
queryset=Period.objects.filter(
project__tasks__date__gte=F('start'),
project__tasks__date__lte=F('end'),
).distinct()
)
)
Это должно эффективно изменить ваш второй запрос от выполнения чего-то вроде
SELECT ... FROM app_period JOIN ...
WHERE apps_period.project_id in (...)
Для
SELECT DISTINCT ... FROM app_period JOIN ...
WHERE apps_period.project_id in (...)
AND apps_tasks.date BETWEEN apps_period.end AND apps_period.start
Комментарии:
1. Я пробовал этот подход, но F ссылается на набор запросов периода, а не на «родительский».
2. Я думаю, что понимаю ваш ответ, но я думаю, что мне также нужно отфильтровать период по проекту часовой задачи
3. Поэтому
to_attr
аргумент при предварительной выборке заполняет объект «project», а не начальный
Ответ №2:
Я нашел следующее решение, но оно немного хакерское, и я не уверен, что закончу его использовать.
Я переопределяю внутренний _fetch_all
метод django QuerySet
, который вызывается при запуске набора запросов. Затем я выполняю пользовательскую предварительную выборку и устанавливаю атрибут экземпляров.
Вероятно, для этого потребуются некоторые дальнейшие оптимизации.
class HourQuerySet(models.query.QuerySet):
# With annotate and Subquery, search and define period_id
def prefetch_period(self):
period_qs = (
Period.objects.filter(
project__tasks=OuterRef("task"), start__lte=OuterRef("date"), end__gte=OuterRef("date")
)
.values("id")[:1]
)
return self.annotate(period_id=Subquery(period_qs))
# Override _fetch_all method to manually prefetch and inject period in returned instances (for which who have a period_id defined)
def _fetch_all(self):
super()._fetch_all()
if not self._result_cache or type(self._result_cache[0]) is dict:
return
period_ids = [r.period_id for r in self._result_cache]
if not period_ids:
return
periods = {p.id: p for p in Period.objects.filter(id__in=period_ids)}
for wh in self._result_cache:
setattr(wh, "period", periods.get(wh.period_id))