Django ManyToManyField Through

Summary: in this tutorial, you’ll learn how to add extra fields to the ManyToManyField using the through argument.

In a many-to-many relationship, multiple rows in a table are associated with multiple rows in another table. To establish a many-to-many relationship, relational databases use a third table called the join table and create two one-to-many relationships from the source tables.

Typically, the join table contains the id values of the source tables so that rows in one table can relate to the rows in the other table.

Sometimes, you may want to add extra fields to the join table. For example, each employee may have multiple jobs during his/her career.

To track when an employee takes a job, you can add the begin_date and end_date fields to the join table.

To do that in Django, you use the ManyToManyField with the through argument.

For example, the following shows how to associate an employee with multiple jobs through the assignments:

class Employee(models.Model):
   # ...

class Job(models.Model):
    title = models.CharField(max_length=255)
    employees = models.ManyToManyField(Employee, through='Assignment')

    def __str__(self):
        return self.title


class Assignment(models.Model):
    employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
    position = models.ForeignKey(Job, on_delete=models.CASCADE)
    begin_date = models.DateField()
    end_date = models.DateField(default=date(9999, 12, 31))Code language: Python (python)

How it works.

  • First, define the Job model, add the employees attribute that uses the ManyToManyField, and pass Assignment as the through argument.
  • Second, define the Assignment class that has two foreign keys, one links to the Employee model, and the other links to the Job model. Also, add the begin_date and end_date attributes to the Assignment model.

Run the makemigrations to make new migrations:

python manage.py makemigrationsCode language: Python (python)

Output:

Migrations for 'hr':
  hr\migrations\0005_assignment_job_assignment_job.py
    - Create model Assignment
    - Create model Job
    - Add field job to assignmentCode language: Python (python)

And execute the migrate command to apply the changes to the database:

python manage.py migrateCode language: Python (python)

Output:

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, hr, sessions
Running migrations:
  Applying hr.0005_assignment_job_assignment_job... OKCode language: Python (python)

Behind the scenes, Django creates the hr_job and hr_assignment tables in the database:

The hr_assignment is the join table. Besides the employee_id and position_id fields, it has the begin_date and end_date fields.

Creating new jobs

First, run the shell_plus command:

python manage.py shell_plusCode language: Python (python)

Second, create three new positions:

>>> j1 = Job(title='Software Engineer I')
>>> j1.save()
>>> j2 = Job(title='Software Engineer II') 
>>> j2.save() 
>>> j3 = Job(title='Software Engineer III')
>>> j3.save()
>>> Job.objects.all()
<QuerySet [<Job: Software Engineer I>, <Job: Software Engineer II>, <Job: Software Engineer III>]>Code language: Python (python)

Creating instances for the intermediate models

First, find the employee with the name John Doe and Jane Doe:

>>> e1 = Employee.objects.filter(first_name='John',last_name='Doe').first()
>>> e1
<Employee: John Doe>
>>> e2 = Employee.objects.filter(first_name='Jane', last_name='Doe').first()
>>> e2
<Employee: Jane Doe>Code language: Python (python)

Second, create instances of the intermediate model (Assignment):

>>> from datetime import date
>>> a1 = Assignment(employee=e1,job=j1, begin_date=date(2019,1,1), end_date=date(2021,12,31))
>>> a1.save()
>>> a2 = Assignment(employee=e1,job=j2, begin_date=date(2022,1,1))
>>> a2.save()
>>> a3 = Assignment(employee=e2, job=j1, begin_date=date(2019, 3, 1))
>>> a3.save()Code language: Python (python)

Third, find the employees that hold the Software Engineer I position (p1):

>>> j1.employees.all()
<QuerySet [<Employee: John Doe>, <Employee: Jane Doe>]>Code language: Python (python)

Behind the scenes, Django executes the following query:

SELECT
  "hr_employee"."id",
  "hr_employee"."first_name",
  "hr_employee"."last_name",
  "hr_employee"."contact_id",
  "hr_employee"."department_id"
FROM "hr_employee"
INNER JOIN "hr_assignment"
  ON ("hr_employee"."id" = "hr_assignment"."employee_id")
WHERE "hr_assignment"."job_id" = 1Code language: Python (python)

Similarly, you can find all employees that hold the Software Engineer II position:

>>> j2.employees.all()
<QuerySet [<Employee: John Doe>]>Code language: Python (python)

Removing instances of the intermediate model instances

First, remove Jane Doe (e2) from the Software Engineer II job using the remove() method:

>>> j2.employees.remove(e2) Code language: Python (python)

Second, remove all employees from Software Engineer I job using the clear() method:

>>> j1.employees.clear() Code language: Python (python)

The j1 job should not have any employees now:

>>> j1.employees.all() 
<QuerySet []>Code language: CSS (css)

Download the source code

Summary

  • Use the through argument in the ManyToManyField to add extra fields to the many-to-many relationship.
Did you find this tutorial helpful ?