Как выполнить предварительную выборку или подзапрос глубоко вложенного объекта с условием в Django ORM

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