Many to Many Relationships

Goals

  1. Describe many-to-many relationships.
  2. Explain how to create M:M relationships in Django.
  3. Skillfully use M:M relationships: add, upsert, query, and such.

Many-to-Many Relationships

What Is a Many-to-Many Relationship?

A many-to-many (M:M) relationship means that:

Examples: - A student can take multiple courses. A course can have multiple students. - A tag can apply to multiple blog posts. A blog post can have multiple tags.

Creating Many-to-Many Relationships in Django

Option 1: Simple M:M with ManyToManyField

from django.db import models

class Student(models.Model):
    name = models.CharField(max_length=100)

class Course(models.Model):
    title = models.CharField(max_length=100)
    students = models.ManyToManyField(Student)

This automatically creates a hidden join table behind the scenes.

Add Records

alice = Student.objects.create(name="Alice")
bob = Student.objects.create(name="Bob")

math = Course.objects.create(title="Math 101")
physics = Course.objects.create(title="Physics 101")

Add Relationships (Students to Courses)**

You can add students to a course using .students.add(…):

math.students.add(alice)         # Alice is enrolled in Math 101
math.students.add(bob)           # Bob is enrolled in Math 101
physics.students.add(bob)        # Bob is enrolled in Physics 101

Or add multiple at once:

physics.students.add(alice, bob)

Courses from a Student

Once added, you can also access related data in reverse:

alice.course_set.all()  # Returns courses Alice is enrolled in

Replace all students

math.students.set([alice])  # Removes all others and sets only Alice

Remove Student from Course

math.students.remove(bob)

6. Clear All Students from Course

math.students.clear()

Option 2: Explicit Through Table for Custom Fields

Use this if you need to store extra information about the relationship.

from django.db import models

class Student(models.Model):
    name = models.CharField(max_length=100)

class Course(models.Model):
    title = models.CharField(max_length=100)
    students = models.ManyToManyField('Student', through='Enrollment')

class Enrollment(models.Model):
    student = models.ForeignKey('Student', on_delete=models.CASCADE)
    course = models.ForeignKey('Course', on_delete=models.CASCADE)
    enrollment_date = models.DateField()

Using Many-to-Many Relationships

Add a Relationship

Simple M:M:

student = Student.objects.get(id=1)
course = Course.objects.get(id=2)
course.students.add(student)

With a Through Table:

Enrollment.objects.create(
    student=student,
    course=course,
    enrollment_date="2025-06-11"
)

Upsert (Update if exists, otherwise insert)

enrollment, created = Enrollment.objects.update_or_create(
    student=student,
    course=course,
    defaults={'enrollment_date': '2025-06-11'}
)

Query Relationships

Simple M:M:

# Get all students in a course
course.students.all()

# Get all courses a student is in
student.course_set.all()  # reverse lookup using default naming

With Through Table:

# Get all enrollments for a student
Enrollment.objects.filter(student=student)

# Get all courses for a student through enrollment
Course.objects.filter(enrollment__student=student)

Remove or Clear Relationships

# Remove one student from a course (simple M:M only)
course.students.remove(student)

# Remove all students from a course
course.students.clear()

Customize Reverse Access

You can customize the reverse access name:

students = models.ManyToManyField(Student, related_name='enrolled_courses')

Then you can access:

student.enrolled_courses.all()

Migrations

After defining or updating your models:

python manage.py makemigrations
python manage.py migrate

Summary