Как использовать prefetch_related для вложенных объектов, связанных с сериализатором, в среде django rest?

#django-rest-framework #query-optimization #django-serializer

Вопрос:

Из приведенной ниже модели я пытаюсь вернуть количество различных типов курсов для конкретного университета в сериализаторе курсов с минимальным количеством запросов, насколько это возможно.

Модель

 class University(models.Model):
    name = models.CharField(max_length=255)

    class Meta:
        ordering = ['name']
        verbose_name_plural = 'Universities'

    def __str__(self):
        return "%s" % self.name



class CourseType(models.Model):
    name = models.CharField("Course Name", max_length=255)

    def __str__(self):
        return self.name


class Course(models.Model):
    name = models.CharField("Course Name", max_length=255)
    course_type = models.ForeignKey(
        CourseType,
        null=True,
        blank=True,
        related_name='course_type',
        on_delete=models.SET_NULL)
    university = models.ForeignKey(
        University,
        null=True,
        blank=True,
        related_name='courses',
        on_delete=models.SET_NULL
    )

    def __str__(self):
        return self.name
 

Сериализатор

 class UniversitySerializer(serializers.ModelSerializer):
    course_type_count = serializers.SerializerMethodField(
        'get_course_type_count')

    class Meta:
        model = University
        fields = "__all__"

    def get_course_type_count(self, obj):
        course_count = {}

        # This one is causing extra query
        courses_type = obj.courses.all().values('course_type__name').annotate(
            total=Count('id')).order_by('course_type')
        for course_type in courses_type:
            course_count[f"{course_type['course_type__name']}"] = course_type['total']
        return course_count


class CourseSerializer(serializers.ModelSerializer):
    course_type = serializers.CharField(source='course_type.name')
    university = UniversitySerializer(read_only=True)

    @staticmethod
    def setup_eager_execution(qs):
        qs = qs.select_related('university')
        qs = qs.select_related('course_type')

        # This is what i've tried
        qs = qs.prefetch_related(
            Prefetch('university__courses__course_type',
                     queryset=qs.prefetch_related('course_type'))
        )


        return qs

    class Meta:
        model = Course
        fields = "__all__"
 

То, что я пробовал.

В сериализаторе курсов я предварительно выбираю тип курса как:

 qs = qs.prefetch_related(
        Prefetch('university__courses__course_type',
                   queryset=qs.prefetch_related('course_type'))
    )
 

В приведенном выше вызове ORM я предварительно выбираю тип курса и перебираю объекты. как показано ниже на UniversitySerializer

 courses_type = obj.courses.all().values('course_type__name').annotate(
   total=Count('id')).order_by('course_type')
 

Все еще есть дубликат запроса на панели инструментов отладки.

Ответ №1:

Одна вещь, о которой вы должны знать, это то, что prefetch_related она работает до тех пор, пока вы обращаетесь .all() к набору запросов или к набору запросов, который соответствует Prefetch набору запросов объектов. В вашем случае get_course_type_count для вычисления количества используется аннотация, поэтому объекты предварительной выборки использовать нельзя. Я предлагаю выполнить расчет количества в python.

 from collections import defaultdict


class UniversitySerializer(serializers.ModelSerializer):
    ...

    def get_course_type_count(self, obj):
        course_count = defaultdict(int)

        for course in obj.courses.all():
            course_count[course.course_type.name]  = 1

        return course_count
 

Этого должно быть достаточно, чтобы просто использовать эту предварительную выборку

 qs = qs.prefetch_related(
    Prefetch('university__courses__course_type')
)