Storing Extra Data in Django Join Tables

I’ve had some people ask me about the through argument supported by Django’s ManyToManyField class. This option supports a very simple use case: when you want to store additional data in a join table.

Imagine, for instance, that we’re building a simple course registration tool. Let’s call the Django app courses. We’ll define the following models:

from django.db import models

class Course(models.Model):
    """A course offered at our school."""
    name = models.CharField(max_length=100)
    code = models.CharField(max_length=5, unique=True)
    description = models.TextField()

    def __unicode__(self):
        return u'%s: %s' % (self.code, self.name)


class Student(models.Model):
    """A student registered with our school."""
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    number = models.CharField(max_length=10, unique=True)
    courses = models.ManyToManyField(Course, null=True)
    registered_on = models.DateTimeField(auto_now_add=True)

    def __unicode__(self):
        return u'%s -> %s, %s' % (
            self.number, self.last_name, self.first_name)

When we run syncdb or generate a migration for these models, Django will automatically create the following tables: courses_course, courses_student, and courses_student_courses. The first two should be self-explanatory. The third table will store information about our many-to-many relationship. Each row will associate a student id with a course id.

Now let’s say we want to store the date and time whenever a student registers for a course. We’d want something like the following model:

class Registration(models.Model):
    """Student registration for a course."""
    student = models.ForeignKey(Student)
    course = models.ForeignKey(Course)
    registered_on = models.DateTimeField(auto_now_add=True)

In order to get Django to use our model as a join table, we can use through by changing a single line in our student model:

class Student(models.Model):
    ...
    courses = models.ManyToManyField(Course, null=True,
        through='Registration')
    ...

And that’s it. Now, Django will automatically generate a courses_registration table and use it instead of courses_student_courses, allowing us to store extra data about the registration.