Search code examples
django-rest-frameworkdjango-orm

django rest framework, how to prefetch_related with a single object


for some reason,I pass an instance(not queryset) to ModelSerializer. but I want to do prefetch_related with this instance.

# instance is a Workout object
instance = queryset.order_by('?').first()
# I want to do something like: instance.prefetch_related('exercises') to avoid N+1 problem
workout = WorkoutModelSerializer(instance)
return Response(workout.data, status=status.HTTP_200_OK)
# custom manytomany through model
class WorkoutExercises(models.Model):
    workout = models.ForeignKey(Workout, on_delete=models.CASCADE)
    exercise = models.ForeignKey(Exercise, on_delete=models.CASCADE)
    rule = models.ForeignKey(Rule, on_delete=models.CASCADE, blank=True)

# ModelSerializer
class WorkoutModelSerializer(serializers.ModelSerializer):
    calorie = SerializerMethodField(help_text='calorie')
    def get_calorie(self, obj):
        calorie = 0
        for exercise in obj.exercises.defer('calorie'):
            calorie += exercise.calorie
        return calorie

    exercise_group = SerializerMethodField()
    def get_exercise_group(self, obj):
        for exercise in obj.exercises.all():
           relation = obj.workoutexercises_set.filter(exercise=exercise).first()
           group_name = relation.rule.group_name 
           # do something with group_name and other rule property ...
     

I use debug_toolbar, It show N+1 problem(maybe I am wrong) enter image description here enter image description here


Solution

  • Here is a complete code from our comments including your last ask:

    instance = (
        queryset.order_by('?')
            .annotate(calorie=Sum(F('exercises__calorie'))  # Get total calories by run `SUM()` and `GROUP BY` and store the result in a new `calorie` field.
            .prefetch_related(
                'workoutexercises_set__rule',               # Prefeth exercises relations, rule, and exercse.
                'workoutexercises_set__exercise',
            )  
    )
    
    # Later in your serializer
    
    class WorkoutModelSerializer(serializers.ModelSerializer):
        calorie = serializers.IntegerField(help_text='calorie')  # Will be taken from new `calorie` aggregated field
    
        exercise_group = SerializerMethodField()
        def get_exercise_group(self, obj):
            for relation in obj.workoutexercises_set.all():      # Relations will be prefetched and the call doesn't lead to N+1.
                exercise = relation.exercise                     # As well as related rule and exercise
                rule = relation.rule
                group_name = relation.rule.group_name 
                # do something with group_name and other rule property ...
    

    Basically, the idea is to delegate compution to the Database where possible and prefetch models you're going to use later in your code.

    One pitfall easy to get into is calling anything beside .all() on prefetched models, since it leads to an extra call to executed.

    I hope it helps, let me know if there is any unclarity.