Оптимизация количества запросов в DRF ModelSerializer - PullRequest
0 голосов
/ 06 июля 2018

В сериализаторе Django Rest Framework можно добавить больше данных в сериализованный объект, чем в исходной модели. Это полезно для расчета статистической информации на стороне сервера и добавления этой дополнительной информации при ответе на вызов API.

Как я понимаю, добавление дополнительных данных выполняется с использованием SerializerMethodField, где каждое поле реализуется с помощью функции get_.... Однако, если у вас есть несколько этих полей SerializerMethodFields, каждое из них может запрашивать модель / базу данных по отдельности, что может быть по существу одинаковыми данными.

Можно ли сделать запрос к базе данных один раз, сохранить список / результат как элемент данных объекта ModelSerializer и использовать результат набора запросов во многих функциях?

Вот очень простой пример, просто для иллюстрации:

############## Model

class Employee(Model):
    SALARY_TYPE_CHOICES = (('HR', 'Hourly Rate'), ('YR', 'Annual Salary'))
    salary_type = CharField(max_length=2, choices=SALARY_TYPE_CHOICES, blank=False)
    salary = PositiveIntegerField(blank=True, null=True, default=0)
    company = ForeignKey(Company, related_name='employees')

class Company(Model):
    name = CharField(verbose_name='company name', max_length=100)


############## View

class CompanyView(RetrieveAPIView):
    queryset = Company.objects.all()
    lookup_field='id'
    serializer_class = CompanySerialiser

class CompanyListView(ListAPIView):
    queryset = Company.objects.all()
    serializer_class = CompanySerialiser


############## Serializer

class CompanySerialiser(ModelSerializer):
    number_employees = SerializerMethodField()
    total_salaries_estimate = SerializerMethodField()
    class Meta:
        model = Company
        fields = ['id', 'name',
                  'number_employees',
                  'total_salaries_estimate',
                 ]
    def get_number_employees(self, obj):
        return obj.employees.count()
    def get_total_salaries_estimate(self, obj):
        employee_list = obj.employees.all()
        salaries_estimate = 0
        HOURS_PER_YEAR = 8*200 # 8hrs/day, 200days/year
        for empl in employee_list:
            if empl.salary_type == 'YR':
                salaries_estimate += empl.salary
            elif empl.salary_type == 'HR':
                salaries_estimate += empl.salary * HOURS_PER_YEAR
        return salaries_estimate

Сериализатор может быть оптимизирован для:

  • использовать элемент данных объекта для сохранения результата из набора запросов,
  • получить набор запросов только один раз,
  • повторно использовать результат набора запросов для всей дополнительной информации, предоставленной в SerializerMethodFields.

Пример:

class CompanySerialiser(ModelSerializer):
    def __init__(self, *args, **kwargs):
        super(CompanySerialiser, self).__init__(*args, **kwargs)
        self.employee_list = None

    number_employees = SerializerMethodField()
    total_salaries_estimate = SerializerMethodField()
    class Meta:
        model = Company
        fields = ['id', 'name',
                  'number_employees',
                  'total_salaries_estimate',
                 ]
    def _populate_employee_list(self, obj):
        if not self.employee_list: # Query the database only once.
            self.employee_list = obj.employees.all()
    def get_number_employees(self, obj):
        self._populate_employee_list(obj)
        return len(self.employee_list)
    def get_total_salaries_estimate(self, obj):
        self._populate_employee_list(obj)
        salaries_estimate = 0
        HOURS_PER_YEAR = 8*200 # 8hrs/day, 200days/year
        for empl in self.employee_list:
            if empl.salary_type == 'YR':
                salaries_estimate += empl.salary
            elif empl.salary_type == 'HR':
                salaries_estimate += empl.salary * HOURS_PER_YEAR
        return salaries_estimate

Это работает для одиночного извлечения CompanyView. И, фактически, сохраняет один запрос / переключение контекста / туда-обратно в базу данных; Я удалил запрос "count".

Тем не менее, не работает для представления списка CompanyListView, потому что кажется, что объект сериализатора создается один раз и повторно используется для каждой компании. Таким образом, в элементе данных «self.employee_list» хранится только первый список сотрудников Компании, и, таким образом, все остальные компании ошибочно получают данные из первой компании.

Существует ли наилучшее практическое решение проблемы такого типа? Или я просто неправ в использовании ListAPIView, и если да, есть ли альтернатива?

Ответы [ 2 ]

0 голосов
/ 20 марта 2019

Как уже упоминалось @Ritesh Agrawal, вам просто нужно предварительно выбрать данные. Однако я советую делать агрегации непосредственно внутри базы данных вместо использования Python:

class CompanySerializer(ModelSerializer):
    number_employees = IntegerField()
    total_salaries_estimate = FloatField()

    class Meta:
    model = Company
    fields = ['id', 'name',
              'number_employees',
              'total_salaries_estimate', ...
             ]

class CompanyListView(ListAPIView):
    queryset = Company.objects.annotate(
       number_employees=Count('employees'),
       total_salaries_estimate=Sum(
           Case(
               When(employees__salary_type=Value('HR'),
                    then=F('employees_salary') * Value(8 * 200)
               ),
               default=F('employees__salary'),
               output_field=IntegerField() #optional a priori, because you only manipulate integers
           )
        )
    )
    serializer_class = CompanySerializer

Примечания:

  • Я не тестировал этот код, но я использую тот же синтаксис для своих собственных проектов. Если вы столкнулись с ошибками (например, «невозможно определить тип вывода» или аналогичными), попробуйте заключить F('employees_salary') * Value(8 * 200) в ExpressionWrapper(..., output_field=IntegerField()).
  • Используя агрегацию, вы можете применить фильтры к набору запросов впоследствии. Однако, если вы предварительно выбираете связанные Employee s, вы больше не можете фильтровать связанные объекты (как упоминалось в предыдущем ответе). НО, если вы уже знаете, что вам нужен список сотрудников с почасовой оплатой, вы можете сделать .prefetch_related(Prefetch('employees', queryset=Employee.object.filter(salary_type='HR'), to_attr="hourly_rate_employees")).

Соответствующая документация: Оптимизация запросов Aggregation

Надеюсь, это поможет вам;)

0 голосов
/ 06 июля 2018

Я думаю, что эта проблема может быть решена, если вы можете передать набор запросов к CompanySerialiser с уже извлеченными данными.

Вы можете сделать следующие изменения

class CompanyListView(ListAPIView):
    queryset = Company.objects.all().prefetch_related('employee_set')
    serializer_class = CompanySerialiser`

И вместо count используйте функцию len, потому что count снова выполняет запрос.

class CompanySerialiser(ModelSerializer):
    number_employees = SerializerMethodField()
    total_salaries_estimate = SerializerMethodField()
    class Meta:
        model = Company
        fields = ['id', 'name',
                  'number_employees',
                  'total_salaries_estimate',
                 ]
    def get_number_employees(self, obj):
        return len(obj.employees.all())
    def get_total_salaries_estimate(self, obj):
        employee_list = obj.employees.all()
        salaries_estimate = 0
        HOURS_PER_YEAR = 8*200 # 8hrs/day, 200days/year
        for empl in employee_list:
            if empl.salary_type == 'YR':
                salaries_estimate += empl.salary
            elif empl.salary_type == 'HR':
                salaries_estimate += empl.salary * HOURS_PER_YEAR
        return salaries_estimate

Поскольку данные предварительно выбраны, сериализатор не будет выполнять никаких дополнительных запросов для all. Но убедитесь, что вы не используете какой-либо фильтр, потому что в этом случае будет выполняться другой запрос.

...