Skip to main content

Fields With Reference to Other Models

It's time to move forward with our course management tool. We need to create a model to save teacher information, so we can have each course assigned to a teacher. Let's start with the model definition by adding in models.py:

class Teacher(models.Model):
name = models.CharField(max_length=128)
full_time = models.BooleanField()

Recall that Django automatically adds to each model an id field that contains a unique identifying number for each instance of a model. Thus, if each course must have a teacher assigned to it, we could create a field in Course that contains its Id:

class Course(models.Model):
name = models.CharField("Name", max_length=128)
enrolled_students = models.IntegerField("Students")
TIMES_OF_DAY = (
(1, "Morning"),
(2, "Afternoon"),
(3, "Evening")
)
time = models.PositiveSmallIntegerField("Time", choices=TIMES_OF_DAY, null=True)
teacher_id = models.PositiveIntegerField()

However, since this is a fairly common situation, Django provides an abstraction of this concept by means of a field type called ForeignKey. So, instead of creating a flat teacher_id integer field, we will do:

class Course(models.Model):
name = models.CharField("Name", max_length=128)
enrolled_students = models.IntegerField("Students")
TIMES_OF_DAY = (
(1, "Morning"),
(2, "Afternoon"),
(3, "Evening")
)
time = models.PositiveSmallIntegerField("Time", choices=TIMES_OF_DAY, null=True)
teacher = models.ForeignKey(Teacher, on_delete=models.SET_NULL, null=True)
note

Make sure the Teacher model definition appears before the Course definition in your models.py, otherwise Python will complain with a NameError: name 'Teacher' is not defined.

As you can see, ForeignKey works like any other field in the definition of a model, but takes the other model to which it refers as argument. The on_delete parameter refers to what should happen if a teacher is deleted from the database while still assigned to a course. In that case, we are indicating that the teacher field must be left empty (NULL), by passing the models.SET_NULL option. For this same reason we make the field accept null values via null=True.

Before seeing the behavior of these changes, make the corresponding migrations:

python manage.py makemigrations myapp
python manage.py migrate myapp

Now open an interactive Django console and make some tests:

>>> from myapp.models import Course, Teacher
>>> teacher_benson = Teacher.objects.create(name="Benson", full_time=True)
>>> django_course = Course.objects.create(name="Django", enrolled_students=12, teacher=teacher_benson)
>>> ml_course = Course.objects.create(name="Machine Learning", enrolled_students=20, teacher=teacher_benson)

Great! We have just created a teacher and assigned two courses to him, which we also created. Now, note how you can access the teacher's fields:

>>> django_course.teacher.name
'Benson'
>>> ml_course.teacher.name
'Benson'
>>> ml_course.teacher.full_time
True
>>> django_course.teacher == ml_course.teacher
True

Another interesting thing is that you can follow the reverse path: retrieve all the courses in which a particular teacher is assigned. To do this, add the related_name attribute to the previous definition of the ForeignKey field in the Course definition:

    teacher = models.ForeignKey(
Teacher,
on_delete=models.SET_NULL,
null=True,
related_name="courses"
)

Close and open again the Django shell so that the model definition is reloaded, and then run:

>>> from myapp.models import Course, Teacher
>>> teacher_benson = Teacher.objects.get(name="Benson")
>>> for course in teacher_benson.courses.all():
... print(course.name, course.enrolled_students)
...
Django 12
Machine Learning 20

You can see that each Teacher instance has a courses attribute that represents the courses to which it is assigned. Under the hood, this expression:

teacher_benson.courses.all()

Is a shortcut for the following filter:

Course.objects.filter(teacher=teacher_benson)