Получение всех данных с помощью одного запроса в 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'
Похоже, это нежелательное поведение, как указано здесь, и может быть исправлено в будущем.