Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 20

Django Model Relationships

Recommended resources

Hello! Welcome back to our course on Backend Engineering.

In our last lesson, we focused on defining the structure of individual models using Django's various field types and constraints. You learned how to ensure data integrity at the lowest level by specifying data types, default values, and validation rules.

Today, we take the crucial next step: connecting these models. The power of a relational database lies in its ability to form a web of interconnected data. Our learning outcome is to implement relationships between models using OneToOneField, ForeignKey, and ManyToManyField. For any complex system, like your AI Career Coach project, this is essential. You need to link users to their interaction histories, job postings to required skills, and so on.

By the end of this lesson, you'll be able to model complex, real-world relationships in your Django application's schema.

1. The One-to-Many Relationship: ForeignKey

This is the most common type of relationship. It connects one object to many other objects.

  • Conceptual Analogy: One User can have many Interactions with your AI coach, but each Interaction belongs to only one User.

How it Works (Under the Hood):
When you use a ForeignKey, Django adds a column to the database table on the "many" side. For our analogy, it would add a user_id column to the interaction table. This column stores the primary key of the related User, creating a "foreign key" constraint at the database level.

Let's watch a short video that introduces the one-to-many concept and shows how to implement it.

Database Relationships | One To Many & Many to Many | Django (3.0) Crash Course Tutorials (pt 6)

The video 'Database Relationships' by Dennis Ivy provides a clear visual explanation of one-to-many relationships and how they are implemented in Django.

Please watch from 00:27 to 01:28 for the conceptual overview of a one-to-many relationship. Then, watch from 04:49 to 07:11 to see how models.ForeignKey is used in code.

As you saw in the video, a ForeignKey field is defined on the "many" side of the relationship. Let's look at the key components:

from django.contrib.auth.models import User
from django.db import models

class Interaction(models.Model):
    # ... other fields for the interaction ...
    
    # This establishes the relationship
    user = models.ForeignKey(
        User,  # The model on the "one" side
        on_delete=models.CASCADE  # What to do when the related user is deleted
    )

The on_delete Argument

This argument is required and is critical for maintaining data integrity. It tells Django what to do with the Interaction object if the User it's linked to is deleted. Common options include:

  • models.CASCADE: If the user is deleted, all their interactions are deleted too. This is the most common behavior.
  • models.SET_NULL: If the user is deleted, the user field on their interactions is set to NULL. This requires you to also set null=True on the ForeignKey field.
  • models.PROTECT: Prevents the user from being deleted as long as they have interactions. Deleting the user would raise a ProtectedError.

Accessing Related Objects

Django automatically creates ways to traverse these relationships.

  • From "many" to "one" (forward): You can directly access the related object as an attribute.
    interaction = Interaction.objects.first()
    user_of_interaction = interaction.user 
    
  • From "one" to "many" (reverse): Django adds a manager to the "one" side, named [model_name]_set by default.
    user = User.objects.first()
    all_interactions_for_user = user.interaction_set.all()
    
    For more readable code, you can customize this name with the related_name argument: user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='interactions'). Then you could use user.interactions.all().

For a great textual summary of ForeignKey, including shell examples, refer to the following resource.

Whats the difference between a OneToOne, ManyToMany ...

This Stack Overflow answer provides a concise and clear explanation of the ForeignKey (many-to-one) relationship with code examples.

Read the section titled 'ForeignKey'. Pay attention to the diagram, the model definitions, and the shell examples showing both forward and reverse relationship access.


2. The One-to-One Relationship: OneToOneField

This relationship ensures that an object is linked to at most one other object, and vice versa. It's like a ForeignKey with a unique=True constraint.

  • Conceptual Analogy: A User has exactly one UserProfile. It's often used to extend an existing model with extra fields without modifying the original model's code.

How it Works (Under the Hood):
This also creates a foreign key column in the database, but it adds a UNIQUE constraint to that column, ensuring no two rows can point to the same related object.

Let's define a profile for your AI Coach users:

from django.contrib.auth.models import User
from django.db import models

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
    bio = models.TextField(blank=True)
    career_goals = models.TextField(blank=True)

Setting primary_key=True is a common and efficient pattern here. It means the Profile table will use the user's ID as its own primary key, avoiding the need for an extra id column.

Accessing the relationship is very direct:

  • user.profile gives you the user's profile.
  • profile.user gives you the profile's user.

3. The Many-to-Many Relationship: ManyToManyField

This is the most flexible relationship type, where an object can be related to many other objects, and those objects can also be related to many of the first type.

  • Conceptual Analogy: For your project, a JobPosting can list many Skills, and a single Skill (e.g., "Python") can be listed on many JobPostings.

How it Works (Under the Hood):
A many-to-many relationship cannot be represented by a single column in either table. Instead, a third table, known as a junction table or intermediary table, is created. This table has two columns: one foreign key for each of the related models. Each row in this table represents a single link between an object from each model.

Django Authentication and Admin Database Schema ERD
This Entity-Relationship Diagram of Django's authentication system shows many-to-many relationships in action. The `auth_user_user_permissions` table is a junction table linking users to permissions.

This video will walk you through the concept and basic implementation.

Database Relationships | One To Many & Many to Many | Django (3.0) Crash Course Tutorials (pt 6)

Let's return to the Dennis Ivy video to see the conceptual breakdown of a many-to-many relationship and how models.ManyToManyField is defined.

First, watch the conceptual explanation from 01:28 to 04:49. Then, see the code implementation from 07:47 to 08:36.

Here is how you would define the JobPosting and Skill models:

class Skill(models.Model):
    name = models.CharField(max_length=100, unique=True)

class JobPosting(models.Model):
    title = models.CharField(max_length=200)
    # ... other fields ...
    skills = models.ManyToManyField(Skill) # You can put this on either model

Querying Many-to-Many Relationships

Because of the junction table, interacting with ManyToManyFields uses a special RelatedManager with methods like add(), remove(), set(), and clear().

To get a deep, practical understanding of how this works, we'll watch a detailed video.

Django ManyToManyFields and Through-Models for many-to-many relationships

The video 'Django ManyToManyFields' from BugBytes offers an excellent, in-depth look at implementing and querying many-to-many relationships. This is where we get into the practical details.

Please watch these segments carefully: Defining the Field (02:08 - 05:59): See how adding a ManyToManyField to a model automatically creates a new junction table in the database. Querying (06:26 - 16:24): This is a critical section. Pay close attention to how the RelatedManager is used with methods like .add(), .remove(), .set(), .clear(), and .filter() to manage the relationships. Reverse Access (16:13 - 18:55): Learn how to query the relationship from the other side using the _set syntax.

Test your understanding!

Imagine you are building the models for your AI Career Coach. You have the following requirements:

  1. A Company can have multiple JobPostings, but each JobPosting belongs to exactly one Company.
  2. A User can save multiple JobPostings as favorites, and a JobPosting can be favorited by multiple Users.

Define the Company, JobPosting, and User models below to represent these relationships. For the User model, assume you're using Django's built-in django.contrib.auth.models.User.

from django.db import models
from django.contrib.auth.models import User

class Company(models.Model):
    name = models.CharField(max_length=100, unique=True)
    # ... other company fields

class JobPosting(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField()
    # Add relationship fields here
    
    # Note: For requirement 2, you can define the ManyToManyField 
    # on either the User or JobPosting model. Let's add it here.
Show answer
from django.db import models
from django.contrib.auth.models import User

class Company(models.Model):
    name = models.CharField(max_length=100, unique=True)
    # ... other company fields

class JobPosting(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField()
    
    # Requirement 1: A JobPosting belongs to one Company.
    company = models.ForeignKey(
        Company, 
        on_delete=models.CASCADE, 
        related_name='job_postings'
    )
    
    # Requirement 2: Users can favorite many JobPostings.
    favorited_by = models.ManyToManyField(
        User,
        related_name='favorite_job_postings',
        blank=True # It's okay for a job to be favorited by no one.
    )

# Explanation:
# 1. We use a `ForeignKey` on `JobPosting` pointing to `Company` to model the one-to-many relationship.
# 2. We use a `ManyToManyField` on `JobPosting` pointing to `User` to model the many-to-many relationship. `blank=True` is added so that a job posting doesn't need to be favorited by anyone to be valid.

4. Advanced: Extra Data on a Many-to-Many Relationship

What if you need to store information about the relationship itself?

  • Analogy: A Student enrolls in a Course. This is a many-to-many relationship. But where do you store the student's grade for that specific course? The grade belongs to neither the student nor the course alone; it belongs to the enrollment.

This is solved by creating a custom intermediary model, specified using the through argument on the ManyToManyField.

Many-to-Many Relationship ERD for HR System
This diagram shows an `hr_assignment` table acting as a custom junction table between `hr_employee` and `hr_job`. It stores extra data about the relationship, like `begin_date` and `end_date`.

The final segment of the BugBytes video covers this advanced but powerful technique.

Django ManyToManyFields and Through-Models for many-to-many relationships

Let's finish the BugBytes video to see how to create and use custom through models for many-to-many relationships.

Introducing through Models (18:44 - 23:29): This part explains the 'why' and 'how' of creating a custom model to act as the junction table, allowing you to add extra fields (like salary in the video's example). Creating Associations (23:29 - 28:35): See how creating relationships changes when using a through model, introducing the through_defaults argument.


Conclusion

You've now learned how to weave your individual Django models into a connected, relational schema. This is the heart of database design and a foundational skill for any backend developer.

Key Takeaways:

  • ForeignKey (One-to-Many): The workhorse of relationships. Defined on the "many" side. Requires an on_delete policy.
  • OneToOneField (One-to-One): Essentially a ForeignKey with a unique constraint. Perfect for extending models like user profiles.
  • ManyToManyField (Many-to-Many): Handles complex relationships where objects on both sides can be linked to multiple partners. Django manages an intermediary junction table for you.
  • through Models: For advanced cases where you need to store extra data on the many-to-many relationship itself, you can define the junction table as your own explicit Django model.

Preview of the Next Lesson:
Our models are now designed and their relationships are defined. But how do we get this structure into our actual database? And how do we manage changes to it over time? In the next lesson, we will focus on creating and applying database migrations to manage your schema and then begin performing Create, Read, Update, and Delete (CRUD) operations using the Django ORM.