Optimizing Nested DRF Serializers

Question:

Help, pliz, to optimize the monstrous code in the serializer, which is very slow … As I understand it, the main problem is in several SerializerMethodFields, where get_tasks () are called. Tell me, please, how to do it right here – in init, load all the tasks that are in self.tasks. And in the to_representation method, all 4 SerializerMethodField call keys – equipment_types, recovery_days, completed_days, work_in_progress_days? And most importantly – how to properly optimize the call of nested serializers?

class MonthPlanViewSerializer(serializers.HyperlinkedModelSerializer):
    year_plan_id = serializers.PrimaryKeyRelatedField(queryset=YearPlan.objects.all(), source='year_plan.id')
    equipment_id = serializers.PrimaryKeyRelatedField(queryset=Equipment.objects.all(), source='equipment.id',
                                                      required=False)
    equipment = EquipmentSerializer(many=False, read_only=True)
    transport_id = serializers.PrimaryKeyRelatedField(queryset=Transport.objects.all(), source='transport.id',
                                                      default=None)
    transport = TransportSerializer(many=False, read_only=True)
    equipment_types = serializers.SerializerMethodField()
    tasks = SimpleTaskSerializer(many=True, read_only=True)
    recovery_days = serializers.SerializerMethodField()
    completed_days = serializers.SerializerMethodField()
    work_in_progress_days = serializers.SerializerMethodField()

    @staticmethod
    def get_tasks(instance: MonthPlan):
        return instance.tasks.all()

    @staticmethod
    def get_work_in_progress_days(instance: MonthPlan) -> set:
        tasks = MonthPlanViewSerializer.get_tasks(instance)
        return set(int(task.planned_date.strftime("%d")) for task in tasks if task.work_in_progress)

    @staticmethod
    def get_completed_days(instance: MonthPlan) -> list:
        return MonthPlanViewSerializer.get_common_days(instance, 'is_completed')

    @staticmethod
    def get_recovery_days(instance: MonthPlan) -> list:
        return MonthPlanViewSerializer.get_common_days(instance, 'is_broken')

    @staticmethod
    def get_common_days(instance: MonthPlan, attribute: str) -> list:
        tasks = MonthPlanViewSerializer.get_tasks(instance)
        days = set()
        for task in tasks:
            for item in task.maintenance_executions:
                if getattr(item, attribute):
                    if task.closing_date:
                        days.add(int(task.closing_date.strftime("%d")))
                    elif task.planned_date:
                        days.add(int(task.planned_date.strftime("%d")))
        return list(days)

    @staticmethod
    def get_equipment_types(instance: MonthPlan):
        tasks = MonthPlanViewSerializer.get_tasks(instance)
        equipment_types = EquipmentType.objects.filter(
            id__in=Equipment.objects.filter(transport_id=instance.transport_id).values_list('equipment_type_id'))

        return EquipmentTypeSerializer(equipment_types, many=True).data

    class Meta:
        model = MonthPlan
        fields = [
            'id', 'year_plan_id', 'month', 'enabled',
            'equipment_id', 'equipment',
            'required_maintenance_quantity',
            'scheduled_maintenance_quantity', 'scheduled_days', 'scheduled_maintenance_quantity_over_year',
            'completed_maintenance_quantity', 'completed_days', 'completed_maintenance_quantity_over_year',
            'recovery_days', 'transport', 'transport_id',
        ]


class MaintenanceCheckTemplateSerializer(serializers.ModelSerializer):
    class Meta:
        model = MaintenanceCheckTemplate
        fields = ['id', 'title', 'description']


class MaintenanceExecutionCheckViewSerializer(serializers.ModelSerializer):
    check = MaintenanceCheckTemplateSerializer(many=False, source='template', read_only=True)

    class Meta:
        model = MaintenanceExecutionCheck
        fields = ['id', 'check', 'value']
        

class SimpleTaskSerializer(serializers.HyperlinkedModelSerializer):
    team_id = serializers.PrimaryKeyRelatedField(
        queryset=Team.objects.all(), source='team.id', allow_null=True
    )
    location = LocationSerializer(many=False, allow_null=True)
    state_id = serializers.PrimaryKeyRelatedField(queryset=State.objects.all(), source='state.id')
    equipment_type_id = serializers.PrimaryKeyRelatedField(
        queryset=EquipmentType.objects.all(), source="equipment_type.id", allow_null=True
    )
    equipment_type = EquipmentTypeSerializer(many=False, allow_null=True)
    maintenance_execution = MaintenanceExecutionSerializer(many=True, read_only=True)
    maintenance_executor = EmployeeSerializer(many=False, read_only=True)

    class Meta:
        model = Task
        fields = [
            'id', 'number', 'description', 'type', 'urgency',
            'location_id', 'location', 'service', 'state_id',
            'equipment_type_id', 'equipment_type', 'team_id', 'maintenance_execution', 'maintenance_executor'
        ]


class MaintenanceExecutionSerializer(serializers.ModelSerializer):
    equipment_type = EquipmentTypeSerializer(many=False, read_only=True)
    checklist = MaintenanceExecutionCheckViewSerializer(many=True)

    class Meta:
        model = MaintenanceExecution
        fields = ['id', 'equipment_type', 'is_completed', 'is_broken', 'comment', 'used_materials', 'checklist']


class EquipmentSerializer(serializers.ModelSerializer):
    location_id = serializers.PrimaryKeyRelatedField(queryset=Location.objects.all(), source='location.id')
    location = LocationSerializer(many=False)
    transport = TransportSerializer(many=False)

    class Meta:
        model = Equipment
        fields = ['id', 'name', 'location_id', 'location', 'transport']


class TransportSerializer(serializers.ModelSerializer):
    location = LocationSerializer(many=False, read_only=True)
    location_id = serializers.PrimaryKeyRelatedField(queryset=Location.objects.all(), source='location.id')

    class Meta:
        model = Transport
        fields = ['id', 'name', 'description', 'manufacturer', 'model', 'status', 'not_working_since', 'location',
                  'location_id']

    def create(self, validated_data):
        for key in ['location']:
            validated_data[key] = validated_data[key]['id']
        instance = Transport.objects.create(**validated_data)
        return instance


class LocationSerializer(serializers.ModelSerializer):
    class Meta:
        model = Location
        fields = ['id', 'name']


class EquipmentType(models.Model):
    name = models.CharField(max_length=200, verbose_name='Наименование')
    maintenance_quantity = models.IntegerField(verbose_name='Норматив обслуживания на 1 шт/год', default=1)
    is_mounted = models.BooleanField(verbose_name='Навесное оборудование', default=False)
    system = models.ForeignKey(System, on_delete=models.RESTRICT, verbose_name='Система', blank=True, null=True)

    class Meta:
        verbose_name = 'тип оборудования'
        verbose_name_plural = 'Типы оборудования'
        ordering = ['name']

    def __str__(self):
        return self.name

Most importantly – how to properly optimize the call of nested serializers?

Answers:

Here are some suggestions for optimizing the code in your serializer:

  1. Preload all tasks in the MonthPlan instance in the init method of the MonthPlanViewSerializer, rather than calling the get_tasks method for each SerializerMethodField. This will avoid making unnecessary database queries for each field.

  2. In the to_representation method, call the appropriate method for each SerializerMethodField (e.g., get_recovery_days) and store the result in a local variable, rather than calling the method multiple times.

  3. For the nested serializers (e.g., EquipmentTypeSerializer), you can use the prefetch_related method to preload related objects when querying the database. This can improve performance by reducing the number of database queries needed to serialize the data.
    Here is an example of how you could apply these suggestions to your code:

    class MonthPlanViewSerializer(serializers.HyperlinkedModelSerializer):
    year_plan_id = serializers.PrimaryKeyRelatedField(
        queryset=YearPlan.objects.all(),
        source='year_plan.id'
    )
    equipment_id = serializers.PrimaryKeyRelatedField(
        queryset=Equipment.objects.all(),
        source='equipment.id',
        required=False
    )
    equipment = EquipmentSerializer(many=False, read_only=True)
    transport_id = serializers.PrimaryKeyRelatedField(
        queryset=Transport.objects.all(),
        source='transport.id',
        default=None
    )
    transport = TransportSerializer(many=False, read_only=True)
    equipment_types = serializers.SerializerMethodField()
    tasks = SimpleTaskSerializer(many=True, read_only=True)
    recovery_days = serializers.SerializerMethodField()
    completed_days = serializers.SerializerMethodField()
    work_in_progress_days = serializers.SerializerMethodField()

    # Preload all tasks for the MonthPlan instance in the __init__ method
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.tasks = self.instance.tasks.all()

    # Return the preloaded tasks
    def get_tasks(self, instance: MonthPlan):
        return self.tasks

    def get_work_in_progress_days(self, instance: MonthPlan) -> set:
        tasks = self.get_tasks(instance)
        return set(int(task.planned_date.strftime("%d")) for task in tasks if task.work_in_progress)

    def get_completed_days(self, instance: MonthPlan) -> list:
        return self.get_common_days(instance, 'is_completed')

    def get_recovery_days(self, instance: MonthPlan) -> list:
        return self.get_common_days(instance, 'is_broken')

To use prefetch_related(), you can specify the related objects that you want to preload when querying the database. You can specify the names of the related objects as arguments to the prefetch_related() method. For example, if you want to preload the team object for each SimpleTask instance, you can use the following code:

class SimpleTaskSerializer(serializers.HyperlinkedModelSerializer):
    team_id = serializers.PrimaryKeyRelatedField(
        queryset=Team.objects.all(),
        source='team.id',
        allow_null=True
    )

    # Use prefetch_related() to preload the team object for each SimpleTask instance
    @staticmethod
    def setup_eager_loading(queryset):
        queryset = queryset.prefetch_related('team')
        return queryset

    class Meta:
        model = SimpleTask
        fields = [
            # Your existing list of fields here
        ]

The setup_eager_loading() method is called by Django Rest Framework when querying the database for the objects that are being serialized. This method allows you to specify how the queryset should be optimized for serialization. In this case, the setup_eager_loading() method uses prefetch_related() to preload the team object for each SimpleTask instance.

Answered By: nklsw