1️⃣ What is Many-to-Many?#

A Many-to-Many relationship means:

One record can be related to many records on the other side

AND

The other side can also relate to many records back

Real-world examples

Example

Meaning

Students ↔ Courses

A student takes many courses, a course has many students

Products ↔ Tags

A product has many tags, a tag belongs to many products

Users ↔ Groups

A user can be in many groups, a group has many users


2️⃣ Mental Model (IMPORTANT)#

Databases do NOT store many-to-many directly

Instead:

A third table (called a junction / join table) is created

Student ←→ Enrollment ←→ Course

3️⃣ Django Models Example#

Student & Course

# models.py

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

    def __str__(self):
        return self.title


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

    def __str__(self):
        return self.name

You wrote only 2 models

Django creates 3 tables


4️⃣ Tables Django Creates (PostgreSQL)#

Table 1: student

CREATE TABLE student (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100)
);

Table 2: course

CREATE TABLE course (
    id SERIAL PRIMARY KEY,
    title VARCHAR(100)
);

Table 3: AUTO-GENERATED JOIN TABLE

Django auto-creates something like:

CREATE TABLE student_courses (
    id SERIAL PRIMARY KEY,
    student_id INTEGER NOT NULL,
    course_id INTEGER NOT NULL,
    UNIQUE (student_id, course_id),
    FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE,
    FOREIGN KEY (course_id) REFERENCES course(id) ON DELETE CASCADE
);

This is the hidden magic table

This is the real many-to-many implementation


5️⃣ What Data Looks Like#

student

id

name

1

Ali

2

Sara

course

id

title

1

Math

2

Physics

student_courses (JOIN TABLE)

id

student_id

course_id

1

1

1

2

1

2

3

2

1

Meaning:
  • Ali → Math, Physics

  • Sara → Math


6️⃣ PostgreSQL Queries (REAL SQL)#

All courses for student “Ali”

SELECT c.title
FROM course c
JOIN student_courses sc ON c.id = sc.course_id
JOIN student s ON s.id = sc.student_id
WHERE s.name = 'Ali';

All students in “Math” course

SELECT s.name
FROM student s
JOIN student_courses sc ON s.id = sc.student_id
JOIN course c ON c.id = sc.course_id
WHERE c.title = 'Math';

7️⃣ Same thing in Django ORM (for comparison)#

Courses of Ali

student = Student.objects.get(name="Ali")
student.courses.all()

Students in Math

course = Course.objects.get(title="Math")
course.student_set.all()

Django ORM hides the join table

PostgreSQL still executes JOINs internally


8️⃣ Custom Many-to-Many (EXTRA IMPORTANT)#

Sometimes you need extra fields in the relationship

(example: enrollment date, grade)

Use through

class Enrollment(models.Model):
    student = models.ForeignKey("Student", on_delete=models.CASCADE)
    course = models.ForeignKey("Course", on_delete=models.CASCADE)
    enrolled_at = models.DateTimeField(auto_now_add=True)
    grade = models.CharField(max_length=2)


class Student(models.Model):
    name = models.CharField(max_length=100)
    courses = models.ManyToManyField("Course", through="Enrollment")

Now the join table is explicit and controllable.


9️⃣ PostgreSQL for custom join table#

CREATE TABLE enrollment (
    id SERIAL PRIMARY KEY,
    student_id INTEGER REFERENCES student(id),
    course_id INTEGER REFERENCES course(id),
    enrolled_at TIMESTAMP,
    grade VARCHAR(2)
);

Key Takeaways (MEMORIZE THIS)

✔ Many-to-Many = 3 tables

✔ Django auto-creates the join table

✔ PostgreSQL uses JOINs, not magic

✔ ManyToManyField is ORM sugar

✔ Use through when relationship has data