Разверните набор 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)