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

What you want to do can be done, for example, using the class Meta method get_fields. The corresponding function will look like this:

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

You can then use, for example, your queryset as shown in the code below and this should work for you. This will fetch all related objects: ManyToMany and ManyToOne for each person object, and attach a list of the results to the prefetch_lookup + '_list' attribute, for example: hobbies_list, shoes_list, and so on.:

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

Or, as Zen of Python advises:

import this

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

You can do this explicitly, for example, by simply listing what you want to extract. These two methods will return similar results:

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

To achieve this in a single query, we can use prefetch_related() to efficiently fetch related objects in a single database hit. However, Django's ORM does not allow returning multiple model types in one QuerySet. The best approach is to fetch the Person objects along with related Hobby, TShirt, Shirt, and Shoes using 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)
Вернуться на верх