python - Expand a QuerySet with all related objects - Stack Overflow

admin2025-04-16  1

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

Given a queryset of Person, e.g.

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

How can I make a queryset which also includes all the objects related to the Person objects in that queryset?

The input QuerySet only has Person objects, but the output one should have Hobby, Shoes, TShirt, Shirt` objects (if there are shirts/tshirts/shoes that reference any of the people in the original queryset).

I've only been able to think of solutions that rely on knowing what the related objects are, e.g. TShirt.objects.filter(person__in=person_queryset), but I would like a solution that will work for all models that reference Person without me having to one-by-one code each query for each referencing model.

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

Given a queryset of Person, e.g.

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

How can I make a queryset which also includes all the objects related to the Person objects in that queryset?

The input QuerySet only has Person objects, but the output one should have Hobby, Shoes, TShirt, Shirt` objects (if there are shirts/tshirts/shoes that reference any of the people in the original queryset).

I've only been able to think of solutions that rely on knowing what the related objects are, e.g. TShirt.objects.filter(person__in=person_queryset), but I would like a solution that will work for all models that reference Person without me having to one-by-one code each query for each referencing model.

Share Improve this question asked Feb 3 at 16:55 NilsNils 3601 silver badge10 bronze badges 5
  • see docs.djangoproject.com/en/dev/ref/models/querysets/… and stackoverflow.com/questions/50193842/… – Anentropic Commented Feb 3 at 18:10
  • Are you stuck on accessing the relationships at all, or are you trying to optimize the query to avoid followups? The first issue part requires using the reverse relation managers (named eg shirt_set by default but you can customize in the ForeignKey declaration) on the Person model, the second requires either select_related or prefetch_related depending on your details. It's possible to iterate through the fields on Person to find relations to add to those methods but kinda painful, generally you are best served by knowing your schema. – Peter DeGlopper Commented Feb 3 at 19:26
  • @PeterDeGlopper I want to avoid having to know what the names of the other models are . shirt_set is only possible to use if I know that there is a relation from Shirt to Person, and then I have to hardcode the shirt_set lookup – Nils Commented Feb 4 at 8:34
  • 1 @Nils that's possible but it's a pain, you're looking at iterating through the get_fields() result inspecting each field to see if it's a related field or not. All the standard ones should be subclasses of django.db.models.fields.related.RelatedField, probably also imported into the django.db.models package but I didn't doublecheck, so you could test for isinstance on that class. Also there are attributes on the field you can inspect to see what kind of relation it is, eg many-to-one or one-to-many. – Peter DeGlopper Commented Feb 4 at 18:59
  • @Nils see github.com/django/django/blob/main/django/db/models/fields/… for the source for that class and github.com/django/django/blob/main/django/db/models/… for some code that uses it (following relations in deletion to see what related models might need to be updated as well). – Peter DeGlopper Commented Feb 4 at 19:01
Add a comment  | 

2 Answers 2

Reset to default 1

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)
转载请注明原文地址:http://www.anycun.com/QandA/1744759948a87223.html