Bird
Raised Fist0
Djangoframework~5 mins

Through model for extra fields on M2M in Django

Choose your learning style10 modes available

Start learning this pattern below

Jump into concepts and practice - no test required

or
Recommended
Test this pattern10 questions across easy, medium, and hard to know if this pattern is strong
Introduction

Sometimes you want to connect two things with extra details about their connection. A through model lets you add those extra details between two related items.

You want to track the date when two items were linked together.
You need to store extra information about the relationship, like a role or status.
You want to customize how two models relate beyond a simple link.
You want to add notes or metadata about the connection between two models.
Syntax
Django
class ModelA(models.Model):
    name = models.CharField(max_length=100)

class ModelB(models.Model):
    name = models.CharField(max_length=100)
    related_as = models.ManyToManyField(ModelA, through='ThroughModel')

class ThroughModel(models.Model):
    modela = models.ForeignKey(ModelA, on_delete=models.CASCADE)
    modelb = models.ForeignKey(ModelB, on_delete=models.CASCADE)
    extra_field = models.CharField(max_length=100)

The through attribute tells Django to use your custom model for the relationship.

In the through model, you must have foreign keys to both related models.

Examples
This example shows authors linked to books with an extra field role describing their job.
Django
class Book(models.Model):
    title = models.CharField(max_length=100)

class Author(models.Model):
    name = models.CharField(max_length=100)
    books = models.ManyToManyField(Book, through='Authorship')

class Authorship(models.Model):
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    role = models.CharField(max_length=50)  # e.g., 'Writer', 'Editor'
This example tracks when a student joined a course using the date_joined field.
Django
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)
    date_joined = models.DateField()
Sample Program

This example shows a sports team and players linked with extra info like when a player joined and their position.

Django
from django.db import models

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

class Team(models.Model):
    name = models.CharField(max_length=100)
    players = models.ManyToManyField(Player, through='Membership')

class Membership(models.Model):
    player = models.ForeignKey(Player, on_delete=models.CASCADE)
    team = models.ForeignKey(Team, on_delete=models.CASCADE)
    date_joined = models.DateField()
    position = models.CharField(max_length=50)

# Usage example (not part of models):
# Create objects and link with extra info
# p = Player.objects.create(name='Alice')
# t = Team.objects.create(name='Red Dragons')
# Membership.objects.create(player=p, team=t, date_joined='2024-01-15', position='Captain')
OutputSuccess
Important Notes

You cannot add extra fields directly on a normal ManyToManyField without a through model.

When using a through model, you manage the relationship by creating and updating the through model instances.

Remember to use on_delete=models.CASCADE to keep data consistent when deleting linked objects.

Summary

A through model lets you add extra details to a many-to-many relationship.

You create a separate model with foreign keys to both related models and extra fields.

Use the through parameter on ManyToManyField to connect your custom model.

Practice

(1/5)
1. What is the main purpose of using a through model in a Django many-to-many relationship?
easy
A. To avoid using foreign keys in models
B. To speed up database queries automatically
C. To create a one-to-one relationship instead
D. To add extra fields to the relationship between two models

Solution

  1. Step 1: Understand many-to-many relationships

    A many-to-many field connects two models but by default stores only the link without extra data.
  2. Step 2: Purpose of a through model

    A through model is a separate model that stores the connection plus extra fields about that connection.
  3. Final Answer:

    To add extra fields to the relationship between two models -> Option D
  4. Quick Check:

    Through model = extra fields on M2M [OK]
Hint: Through model = extra info on many-to-many link [OK]
Common Mistakes:
  • Thinking through model speeds up queries
  • Confusing through model with one-to-one relationships
  • Believing through model removes foreign keys
2. Which of the following is the correct way to declare a many-to-many field using a through model named Membership in Django when the Membership model is defined later?
easy
A. members = models.ManyToManyField(User, through='Membership')
B. members = models.ManyToManyField(User, through=Membership())
C. members = models.ManyToManyField(User, through=Membership)
D. members = models.ManyToManyField(User, through='membership')

Solution

  1. Step 1: Syntax for through argument

    The through argument expects the model name as a string if the model is defined later or in the same app.
  2. Step 2: Correct usage

    Using 'Membership' as a string is correct. Passing the class or instance directly is incorrect.
  3. Final Answer:

    members = models.ManyToManyField(User, through='Membership') -> Option A
  4. Quick Check:

    through='ModelName' string syntax [OK]
Hint: Use model name as string in through argument [OK]
Common Mistakes:
  • Passing model class or instance instead of string
  • Using lowercase model name string
  • Omitting the through argument
3. Given the models below, what will print(membership.role) output?
class Group(models.Model):
    name = models.CharField(max_length=100)

class User(models.Model):
    username = models.CharField(max_length=100)

class Membership(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    role = models.CharField(max_length=50)

# Usage
user = User(username='alice')
user.save()
group = Group(name='Developers')
group.save()
membership = Membership(user=user, group=group, role='admin')
membership.save()
print(membership.role)
medium
A. Error: role field missing
B. alice
C. admin
D. Developers

Solution

  1. Step 1: Understand Membership model fields

    Membership has a role field storing a string like 'admin'.
  2. Step 2: Check the saved membership instance

    Membership instance is created with role='admin', so printing membership.role outputs 'admin'.
  3. Final Answer:

    admin -> Option C
  4. Quick Check:

    membership.role = 'admin' [OK]
Hint: Print the extra field on through model instance [OK]
Common Mistakes:
  • Confusing role with user or group fields
  • Expecting username or group name instead
  • Assuming role field is missing
4. What is wrong with this through model declaration?
class User(models.Model):
    username = models.CharField(max_length=100)

class Membership(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    group = models.ForeignKey('Group', on_delete=models.CASCADE)
    role = models.CharField(max_length=50)

class Group(models.Model):
    name = models.CharField(max_length=100)
    members = models.ManyToManyField(User, through='Membership')
medium
A. Membership model is declared before Group, causing a NameError
B. No error; this is a valid declaration
C. The through model must be declared after both related models
D. ForeignKey fields in Membership must use related_name

Solution

  1. Step 1: Check model declaration order

    Membership can be declared before Group if the through argument uses string 'Membership'.
  2. Step 2: Validate through usage

    Using through='Membership' is correct and avoids circular import or NameError.
  3. Final Answer:

    No error; this is a valid declaration -> Option B
  4. Quick Check:

    through='ModelName' string allows any order [OK]
Hint: Use string name for through to avoid order errors [OK]
Common Mistakes:
  • Thinking model order causes NameError with string through
  • Believing related_name is mandatory for ForeignKey
  • Assuming through model must be after both models
5. You want to track the date a user joined a group using a through model. Which of these is the best way to add this feature?
hard
A. Add a date_joined = models.DateField() field to the through model and use through='Membership' in the many-to-many field
B. Add a date_joined field directly to the User model
C. Add a date_joined field directly to the Group model
D. Use a signal to store the date_joined in a separate table unrelated to the many-to-many

Solution

  1. Step 1: Identify where to store extra relationship data

    Extra info about the user-group link belongs in the through model, not in User or Group alone.
  2. Step 2: Add date_joined field to through model

    Adding date_joined to Membership and linking with through='Membership' is the correct pattern.
  3. Final Answer:

    Add a date_joined = models.DateField() field to the through model and use through='Membership' -> Option A
  4. Quick Check:

    Extra data on M2M = through model field [OK]
Hint: Extra data on M2M? Put field in through model [OK]
Common Mistakes:
  • Adding extra fields to User or Group instead of through model
  • Using signals unnecessarily for simple data
  • Not linking through model in many-to-many field