How to Count Reverse ManyToMany Relationships with Django REST Framework

How to Count Reverse ManyToMany Relationships with Django REST Framework

Sometimes it's useful to count the numbers of reverse m2m relationships for each specific object of a queryset.

Normally with Django you would do this at the view level and it would automatically be available for you in the templates, through Django's template engine.

#views.py

from django.db.models import Count

queryset = Movie.objects.annotate(num_actors=Count('actors'), num_comments=Count('comments'), num_likes=Count('likes')).order_by('-created_at')

In the view above, num_actors specifies the field, and actors specifies the reverse m2m, in this case with a related_name='actors' specified at the model level.

.annotate(num_actors=Count('actors')

Without a specified reletade_name at the model level you can still reach and count the actors with actor_set.

.annotate(num_actors=Count('actor_set')

At the surface we can achieve the same results With Django REST Framework by specifying a read_only IntegerField's with a source to the reverse ManytoMany relationship.

#serializers.py

class MovieListSerializer(serializers.ModelSerializer):
    num_actors = serializers.IntegerField(source='actors.count', read_only=True)
    num_comments = serializers.IntegerField(source='comments.count', read_only=True)
    num_likes = serializers.IntegerField(source='likes.count', read_only=True)

    class Meta:
        model = Movie
        fields = [
            'created_at',
            'updated_at',
            'title',
            'slug',
            'image',
            'description',
            'num_actors',
            'num_comments',
            'num_likes',
        ]

However, there is a problem with this solution. It will introduce an N+1 problem where the database is hit for every result that is returned, simply put, it does not scale well. As the database grows so does the time it takes to fetch objects from your API.

What we need to use a combination of the two and use Annotations (as was used in the regular Django example). So instead of using the serializer to find the source we simply specify the IntegerFields as is, and annotate in the view.

#serializers.py

class MovieListSerializer(ListAPIView):
    num_actors = serializers.IntegerField(read_only=True)
    num_comments = serializers.IntegerField(read_only=True)
    num_likes = serializers.IntegerField(read_only=True)

    class Meta:
        model = Movie
        fields = [
            'created_at',
            'updated_at',
            'title',
            'slug',
            'image',
            'description',
            'num_actors',
            'num_comments',
            'num_likes',
        ]

As you would do normally in Django, we annotate and Count each relationship at the view-level.

#views.py

from django.db.models import Count

queryset = Movie.objects.annotate(num_actors=Count('actors'), num_comments=Count('comments'), num_likes=Count('likes')).order_by('-created_at')

It is always recommended to specify related_name at the model level as such:

class Actor(models.Model):
    created_by          = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
    updated_by          = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="+")
    created_at          = models.DateTimeField(auto_now_add=True)
    updated_at          = models.DateTimeField(auto_now=True)
    title               = models.CharField(max_length=120)
    slug                = AutoSlugField(populate_from='title', unique=True, max_length=120)
    image               = models.ImageField(upload_to='actors', null=True, blank=True)
    description         = RichTextField(max_length=2000, null=True, blank=True)
    movies            	= models.ManyToManyField('cinema.Movie', related_name='actors')

    class Meta:
        verbose_name = 'Actor'
        verbose_name_plural = 'Actors'

    def __str__(self):
        return self.title
movies = models.ManyToManyField('cinema.Movie', related_name='actors')

The related name makes it easy to query your data in a more readable way. We can now reach the relationship with movies.actors:

#like the following examples:
movies.actors
num_actors = serializers.IntegerField(source='actors.count', read_only=True)
queryset = Movie.objects.annotate(num_actors=Count('actors'), num_comments=Count('comments'), num_likes=Count('likes')).order_by('-created_at')
#instead of the following examples:
movies.actor_set
num_actors = serializers.IntegerField(source='actor_set.count', read_only=True)
queryset = Movie.objects.annotate(num_actors=Count('actor_set'), num_comments=Count('comment_set'), num_likes=Count('like_set')).order_by('-created_at')
Freddie Freddie 2 years, 7 months ago 0
Did you enjoy Freddie Freddie's article?
How to Count Reverse ManyToMany Relationships with Django REST Framework
Login to Comment
No comments have been posted yet, be the first one to comment.
Query your Django REST API like a GraphQL API with Django RESTQL
Query your Django REST API like a GraphQL API with Django RESTQL
The hype around GraphQL in recent years has been hard to ignore for any web developer. In short, GraphQL is a query language and runtime for APIs, and it has taken the web with storm. GraphQL makes it possible for front-end developers to query data from a single API endpoint and retri...
How to Cache Django REST Framework with Redis
How to Cache Django REST Framework with Redis
Django REST Framework is an awesome package that will aid you in writing REST APIs faster with Django and Python. Though Django REST Framework has many strengths, performance out-of-the-box might not be one of them. However, there are many ways to fix that, and one of them is caching....
How to Rebuild Django-MPTT Tree Structure
How to Rebuild Django-MPTT Tree Structure
Most application utilize some sort of tree structure to managing data, in one form or another. One common use-case is nested categories. If you are using Django for your current project and are in need to implement tree structures, there is a big change you have come across Django-MPT...