Expand a QuerySet with all related objects
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.
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)