How to use the Many-to-Many field in your Django models

What is a Many-to-Many field?

A many-to-many field is a type of field used to establish a link between two database tables where one row can be linked to multiple rows of another table and vice versa.

Example 1: Tagging

Consider an image-sharing app. Users can assign multiple tags to their posts and they can view multiple posts for a given tag.

from django.db import models

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

class Post(models.Model):
    image_path = models.CharField(max_length=255)
    caption = models.TextField()
    tags = models.ManyToManyField(to=Tag, related_name="posts")

Example 2: Movies and Actors

Consider a movie database. There is a table of movies and a table of actors. Users want to see the cast of each movie and all the movies that an actor has starred in.

class Actor(models.Model):
    first_name = models.CharField(max_length=255)
    last_name = models.CharField(max_length=255)

    def __str__(self):
        return f"{self.first_name} {self.last_name}"


class Movie(models.Model):
    title = models.CharField(max_length=255)
    cast = models.ManyToManyField(to=Actor, related_name="movies")
    year = models.PositiveIntegerField()

    def __str__(self):
        return f"{self.title}"

Which model should contain the field?

The many-to-many field must only be set on one of the tables, never both.

In the movie example, we set the ManyToManyField on the Movie model but not Actor.

We don’t need to. On the many-to-many field we set the related_name. By setting related_name to “movies”, we can access the movies of an actor through the movies attribute.

In this snippet, I’ve got a movie called when_harry_met_sally and an actor meg_ryan.

If I want to see the cast of the film When Harry Met Sally, I can do this:

>>> when_harry_met_sally.cast.all()
<QuerySet [<Actor: Meg Ryan>, <Actor: Billy Crystal>]>

And if I want to see the movies Meg Ryan has starred in, I can do this:

>>> meg_ryan.movies.all()
<QuerySet [<Movie: Sleepless in Seattle>, <Movie: When Harry Met Sally>]>

How to use Many-to-Many fields

You cannot add the links when instantiating an object. You have to add them after.

This is how you add actors to a movie’s cast.

>>> from examples.models import Actor, Movie

>>> meg_ryan = Actor.objects.create(first_name="Meg", last_name="Ryan")
>>> billy_crystal = Actor.objects.create(first_name="Billy", last_name="Crystal")
>>> carrie_fisher = Actor.objects.create(first_name="Carrie", last_name="Fisher")

>>> movie = Movie.objects.create(title="When Harry Met Sally")

>>> movie.cast.all()
<QuerySet []>

>>> movie.cast.add(meg_ryan, billy_crystal, carrie_fisher)

>>> movie.cast.all()
<QuerySet [<Actor: Meg Ryan>, <Actor: Billy Crystal>, <Actor: Carrie Fisher>]>

In the snippet, we create three actors: Meg Ryan, Billy Crystal and Carrie Fisher.

We also create a movie, When Harry Met Sally, but its cast is an empty queryset.

We set the cast by doing movie.cast.add(.... We can add multiple actors to the cast at a time.

How to Query Many-to-Many fields

How would I query for movies starring Meg Ryan or Tom Hanks?

If you’re not familiar with either actor, here are their films:

A quick reference if you're not familiar with 90's movies. Meg Ryan starred in When Harry Met Sally and You've got Mail. Tom Hanks starred in Forrest Gump and You've Got Mail.

>>> tom_hanks.movies.all()
<QuerySet [<Movie: You've Got Mail>, <Movie: Forrest Gump>]>

>>> meg_ryan.movies.all()
<QuerySet [<Movie: You've Got Mail>, <Movie: When Harry Met Sally>]>

>>> Movie.objects.filter(cast__in=[tom_hanks, meg_ryan])
<QuerySet [<Movie: You've Got Mail>, <Movie: When Harry Met Sally>, <Movie: You've Got Mail>, <Movie: Forrest Gump>]>

You’ve Got Mail, which stars both actors, appears twice. We can use distinct() to remove the duplicate.

>>> Movie.objects.filter(cast__in=[tom_hanks, meg_ryan]).distinct()
<QuerySet [<Movie: You've Got Mail>, <Movie: When Harry Met Sally>, <Movie: Forrest Gump>]>

Query movies starring just Tom Hanks

There are three ways to do this.

Option 1: Query the Actor table:

>>> tom_hanks.movies.all()
<QuerySet [<Movie: You've Got Mail>, <Movie: Forrest Gump>]>

Option 2: Query the Movie table with the tom_hanks instance

>>> Movie.objects.filter(cast=tom_hanks)
<QuerySet [<Movie: You've Got Mail>, <Movie: Forrest Gump>]>

Option 3: Query the Movie table by first and last name

>>> Movie.objects.filter(cast__first_name="Tom", cast__last_name="Hanks")
<QuerySet [<Movie: You've Got Mail>, <Movie: Forrest Gump>]>

Query movies starring both Tom Hanks and Meg Ryan

You can do this by chaining filters:

>>> Movie.objects.filter(cast=meg_ryan).filter(cast=tom_hanks)
<QuerySet [<Movie: You've Got Mail>]>

A different way to chain filters:

>>> actors = [tom_hanks, meg_ryan]
>>> movies = Movie.objects.all()
>>> for actor in actors:
...     movies = movies.filter(cast=actor)
... 
>>> movies
<QuerySet [<Movie: You've Got Mail>]>

How to use the Many to Many Manager

For the next set of examples, I’ve created a row in the Actor table for Carrie Fisher. I also created 9 movies that star Carrie Fisher:

carrie_fisher = Actor.objects.create(first_name="Carrie", last_name="Fisher")

Notice how the movies attribute of an actor isn’t helpful?

>>> carrie_fisher.movies
<django.db.models.fields.related_descriptors.create_forward_many_to_many_manager.<locals>.ManyRelatedManager object at 0x1090c6fa0>

The many-to-many manager works like a queryset. This means you can apply the normal queryset operators.

All Carrie Fisher movies

>>> carrie_fisher.movies.all()
<QuerySet [<Movie: Star Wars: The Rise of Skywalker>, <Movie: Star Wars: The Last Jedi>, <Movie: Star Wars: The Force Awakens>, <Movie: The Women>, <Movie: Hollywood Vice Squad>, <Movie: Star Wars: Return of the Jedi>, <Movie: Star Wars: The Empire Strikes Back>, <Movie: Star Wars>, <Movie: When Harry Met Sally>]>

The total number of movies [in the db] starring Carrie Fisher

>>> carrie_fisher.movies.count()
9

All the Carrie Fisher movies with Star Wars in the title

>>> carrie_fisher.movies.filter(title__icontains="star wars")
<QuerySet [<Movie: Star Wars: The Rise of Skywalker>, <Movie: Star Wars: The Last Jedi>, <Movie: Star Wars: The Force Awakens>, <Movie: Star Wars: Return of the Jedi>, <Movie: Star Wars: The Empire Strikes Back>, <Movie: Star Wars>]>

>>> carrie_fisher.movies.filter(title__icontains="star wars").count()
6

Conclusion

Many-to-many fields are fields you can use to link two models, where a row in one table can be linked to multiple rows in another table and vice versa.

Examples of this include tagging of posts, where a post can have multiple tags and tags can have multiple posts.

Another example is the link between movies and actors. Movies have multiple actors in their cast and an actor will have multiple movies in their filmography.

Many-to-many fields can be defined on models using the models.ManyToManyField. The field just has to be defined on one of the models, the related_name option lets you specify the name of the attribute on the other model.

We can create links between models by the add method. We can access the field like a normal attribute but we have to treat it like a queryset, using operations like .all() or .filter to access the linked objects.