Разверните набор QuerySet со всеми связанными объектами

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


class Person(models.Model):
    name = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    hobbies = models.ManyToManyField(Hobby, related_name='persons')


class TShirt(models.Model):
    name = models.TextField()
    person = models.ForeignKey(
        Person,
        related_name='tshirts',
        on_delete=models.CASCADE,
    )


class Shirt(models.Model):
    name = models.TextField()
    person = models.ForeignKey(
        Person,
        related_name='shirts',
        on_delete=models.CASCADE,
    )


class Shoes(models.Model):
    name = models.TextField()
    person = models.ForeignKey(
        Person,
        related_name='shoes',
        on_delete=models.CASCADE,
    )

Задается кверисет из Person, например

Person.objects.order_by('-created_at')[:4]

Как сделать набор запросов, который также включает все объекты, связанные с объектами Person в этом наборе запросов?

Во входном QuerySet есть только Person объекты, а в выходном должны быть Hobby, Shoes, TShirt, Shirt` объекты (если есть рубашки/рубашки/обувь, которые ссылаются на любого из людей в исходном queryset).

Я смог придумать только те решения, которые зависят от знания того, что представляют собой связанные объекты, например TShirt.objects.filter(person__in=person_queryset), но я хотел бы получить решение, которое будет работать для всех моделей, ссылающихся на Person, без того, чтобы мне приходилось по одному кодировать каждый запрос для каждой ссылающейся модели.

То, что вы хотите сделать, можно сделать, например, с помощью класса Meta метод get_fields. Соответствующая функция будет выглядеть следующим образом:

from django.db import models


def get_non_hidden_m2o_and_m2m_prefetch_lookups(model):
    result = []
    for field in model._meta.get_fields():
        if not isinstance(field, models.ManyToOneRel | models.ManyToManyField):
            continue

        try:
            result.append(field.related_name or field.name + '_set')
        except AttributeError:
            result.append(field.name)

    return result

Затем вы можете использовать, например, свой queryset, как показано в приведенном ниже коде, и это должно сработать у вас. Это позволит получить все связанные объекты: ManyToMany и ManyToOne для каждого объекта person и прикрепить список результатов к атрибуту prefetch_lookup + '_list', например: hobbies_list, shoes_list, и так далее.:

prefetch_lookups = get_non_hidden_m2o_and_m2m_prefetch_lookups(model=Person)
prefetch_args = [(lookup, lookup + '_list') for lookup in prefetch_lookups]
queryset = (
    Person
    .objects
    .prefetch_related(
        *(
            models.Prefetch(lookup=lookup, to_attr=to_attr)
            for lookup, to_attr in prefetch_args
        )
    )
    .order_by('-created_at')[:4]
)
for person in queryset:
    print(person)
    for _, attr in prefetch_args:
        print(attr , getattr(person, attr))

Или, как советует Zen of Python:

import this

...
Explicit is better than implicit.
...

Вы можете сделать это явно, например, просто указав, что вы хотите извлечь. Эти два метода вернут похожие результаты:

prefetch_objects = (  
    models.Prefetch(lookup='hobbies', to_attr='hobbies_list'),  
    models.Prefetch(lookup='tshirts', to_attr='tshirts_list'),  
    models.Prefetch(lookup='shirts', to_attr='shirts_list'),  
    models.Prefetch(lookup='shoes', to_attr='shoes_list'),  
)  
queryset = (  
    Person.objects  
    .prefetch_related(*prefetch_objects)  
    .order_by('-created_at')[:4]  
)

Чтобы достичь этого в одном запросе, мы можем использовать prefetch_related() для эффективной выборки связанных объектов в одном обращении к базе данных. Однако ORM в Django не позволяет возвращать несколько типов моделей в одном наборе запросов. Наилучший подход - извлечь объекты Person вместе со связанными хобби, футболкой и обувью, используя функцию prefetch_related()

persons = Person.objects.order_by('-created_at').prefetch_related(
'hobbies',  # ManyToManyField
'tshirts',  # Reverse ForeignKey
'shirts',
'shoes')

for person in persons:
print(f"Person: {person.name}")
print("  Hobbies:", [h.name for h in person.hobbies.all()])
print("  TShirts:", [t.name for t in person.tshirts.all()])
print("  Shirts:", [s.name for s in person.shirts.all()])
print("  Shoes:", [sh.name for sh in person.shoes.all()])
print("-" * 40)
Вернуться на верх