Получение всех данных с помощью одного запроса в Django

Как получить все объекты, связанные с детьми, в одном запросе в Django?

Например, у нас есть 3 модели:

class Parent(models.Model):
  ...

class Child(models.Model):
  parent = models.ForeignKey(Parent)

class SubChild(models.Model):
  parent = models.ForeignKey(Child)

Я хочу получить все данные в одном запросе следующим образом:

select * from parent p join child c on c.parent_id = p.id join subchild sc on sc.parent_id = c.id

Как я могу сделать это с помощью Django ORM? Что если у меня будет несколько дочерних моделей? В SQL я просто добавляю дополнительные джойны, но как я могу сделать это в Django?

Теперь у меня есть несколько секунд задержки при рекурсивной загрузке всего с несколькими запросами. Метод prefetch_related не помогает, я имею в виду

qs = Parent.objects.all().prefetch_related('child').prefetch_related('child__subchild')

for parent in qs:
  # here I need to access parent.childs.all() and child.subchild.all() without new queries

Вы получили ответ на первую часть вашего вопроса, и я хотел обратиться ко второй части - как мы можем предварительно получить данные с несколькими дочерними моделями. Мы можем получить списки значений, если не сами объекты. Например, у нас есть такие модели - родительский класс, два дочерних класса с внешними ключами к нему и их дочерние классы с внешними ключами соответственно:

class Parent(models.Model):
  name = models.TextField()

class Child1(models.Model):
  parent = models.ForeignKey(Parent, on_delete=models.PROTECT)
  name = models.TextField()

class SubChild1(models.Model):
  parent = models.ForeignKey(Child1, on_delete=models.PROTECT)
  name = models.TextField()

class Child2(models.Model):
  parent = models.ForeignKey(Parent, on_delete=models.PROTECT)
  name = models.TextField()

class SubChild2(models.Model):
  parent = models.ForeignKey(Child2, on_delete=models.PROTECT)
  name = models.TextField()

Используя эти модели, мы можем получить списки значений дочерних элементов с нужными столбцами, включая родительский и родительский столбцы, затем объединить эти списки значений - получится один запрос с внутренними соединениями. Таким образом, этот код

 grandchildren1 = SubChild1.objects.all().values_list(
     'id', 'name', 'parent__name', 'parent__parent__name'
     ).prefetch_related('parent').prefetch_related('parent')
 grandchildren2 = SubChild2.objects.all().values_list(
     'id', 'name', 'parent__name', 'parent__parent__name'
     ).prefetch_related('parent').prefetch_related('parent')
 grandchildren = grandchildren1.union(grandchildren2)

результаты этого единственного запроса:

SELECT "app_subchild1"."id" AS "col1", 
     "app_subchild1"."name" AS "col2", 
     "app_child1"."name" AS "col3", 
     "app_parent"."name" AS "col4" 
 FROM "app_subchild1" 
 INNER JOIN "app_child1" ON (
     "app_subchild1"."parent_id" = "app_child1"."id"
 ) 
 INNER JOIN "app_parent" ON (
     "app_child1"."parent_id" = "app_parent"."id"
 ) 
 UNION 
 SELECT "app_subchild2"."id" AS "col1", 
     "app_subchild2"."name" AS "col2", 
     "app_child2"."name" AS "col3", 
     "app_parent"."name" AS "col4" 
 FROM "app_subchild2" 
 INNER JOIN "app_child2" ON (
     "app_subchild2"."parent_id" = "app_child2"."id"
 ) 
 INNER JOIN "app_parent" ON (
     "app_child2"."parent_id" = "app_parent"."id"
 )

Если вам нужны не объекты внуков, а их данные, можно использовать этот подход.

Мы также можем предварительно получить оба дочерних объекта и их детей тоже, а затем объединить оба кверисета - то есть, мы не получим ошибки, но это не работает так, как задумано: префетч первого кверисета работает, а второго молча - нет. Этот кверисет:

qs = (Parent.objects.all()
    .prefetch_related('child1_set')
    .prefetch_related('child1_set__subchild1_set')
    .union(
    Parent.objects.all()
    .prefetch_related('child2_set')
    .prefetch_related('child2_set__subchild2_set')
    )) 

приводит к таким запросам:

'SELECT "app_parent"."id", "app_parent"."name" 
 FROM "app_parent"'

'SELECT "app_child1"."id", "app_child1"."parent_id", "app_child1"."name" 
 FROM "app_child1" 
 WHERE "app_child1"."parent_id" IN (1, 2, 3)'

'SELECT "app_subchild1"."id", "app_subchild1"."parent_id", "app_subchild1"."name" 
 FROM "app_subchild1" 
 WHERE "app_subchild1"."parent_id" IN (1, 2, 3)'

'SELECT "app_child2"."id", "app_child2"."parent_id", "app_child2"."name" 
 FROM "app_child2" 
 WHERE "app_child2"."parent_id" = 1'

'SELECT "app_child2"."id", "app_child2"."parent_id", "app_child2"."name"
 FROM "app_child2" 
 WHERE "app_child2"."parent_id" = 2'

'SELECT "app_child2"."id", "app_child2"."parent_id", "app_child2"."name"
 FROM "app_child2" 
 WHERE "app_child2"."parent_id" = 3'

'SELECT "app_subchild2"."id", "app_subchild2"."parent_id", "app_subchild2"."name" 
 FROM "app_subchild2" 
 WHERE "app_subchild2"."parent_id" = 1'

'SELECT "app_subchild2"."id", "app_subchild2"."parent_id", "app_subchild2"."name" 
 FROM "app_subchild2" 
 WHERE "app_subchild2"."parent_id" = 2'

Похоже, это нежелательное поведение, как указано здесь, и может быть исправлено в будущем.

Вернуться на верх