Skip to main content

Welcome to a Grasp lesson

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

Lesson 21

Defining Model Relationships

Recommended resources

Hello! Welcome back.

In the last lesson, you mastered Django migrations, learning how to translate your Python models into a database schema. You now have a tasks_task table in your database, ready to hold data. However, so far, our Task model exists in isolation.

For our task manager app to be useful, a task needs to belong to a specific user. Similarly, we might want to categorize tasks with tags like "Work," "Personal," or "Urgent." This requires creating relationships between different pieces of data.

This lesson is all about building those connections. You'll learn how to establish relationships between models using ForeignKey and ManyToManyField. These are the fundamental tools for creating a well-structured and interconnected web application.

1. The One-to-Many Relationship: ForeignKey

The most common type of relationship is "one-to-many." Think about it in the context of our project:

  • One user can have many tasks.
  • But one task belongs to only one user.

This is a classic one-to-many relationship. Django represents this using a ForeignKey field.

To get a clear conceptual overview, let's start with a short video.

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, concise explanation of the one-to-many relationship concept using a customer-and-order example.

Watch from 00:27 to 01:28. Pay attention to how the orders table holds a reference (a foreign key) to the customers table to link an order to a specific customer. This is exactly what we're about to do with tasks and users.

As the video explained, the "many" side of the relationship holds a key that points to the "one" side. In our case, the Task model will hold a key pointing to the User model.

This diagram shows the concept visually:

Django ForeignKey One-to-Many Relationship Illustration
This image illustrates a one-to-many relationship. Just like an employee belongs to one division, a task will belong to one user.

Implementing ForeignKey

Let's add this relationship to our Task model. We'll link it to Django's built-in User model, which handles authentication for us (we'll cover that in detail later).

First, you need to import the User model at the top of your tasks/models.py file.

from django.contrib.auth.models import User

Now, add the ForeignKey field to your Task model.

# tasks/models.py

from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User # Add this import

class Task(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField(null=True, blank=True)
    completed = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    due_date = models.DateField(null=True, blank=True)
    user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True) # Add this line

    def __str__(self):
        return self.title

    class Meta:
        ordering = ['created_at']

Note: For now, we're adding null=True and blank=True. This makes the user optional for now, which simplifies things as we build. Later, we'll make it mandatory.

The on_delete Parameter

The on_delete=models.CASCADE part is crucial. It tells Django what to do if the user associated with a task is deleted.

  • models.CASCADE: If the user is deleted, delete all of their tasks as well. This is a common and logical default for this kind of relationship.

There are other options, like models.SET_NULL (which would set the task's user field to NULL) or models.PROTECT (which would prevent the user from being deleted if they still have tasks). The MDN documentation provides a good summary of these options.

Apply Your Changes

Now that you've modified your model, you need to update the database. You already know how to do this from our last lesson!

  1. Generate the migration file:

    python manage.py makemigrations tasks
    

    Django will detect the new user field and create a new migration file.

  2. Apply the migration:

    python manage.py migrate
    

    This command will execute the ALTER TABLE statement to add the new user_id column to your tasks_task table in the database.


2. The Many-to-Many Relationship: ManyToManyField

What if a relationship can go both ways? For example:

  • One task could have many tags (e.g., "Urgent", "Work").
  • One tag could be applied to many different tasks.

This is a "many-to-many" relationship. Django handles this with a ManyToManyField.

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 understand the concept behind many-to-many relationships.

Watch from 01:28 to 04:49. The key takeaway is that a many-to-many relationship requires a third, intermediate table (or "join table") to keep track of the connections. The good news is, Django creates and manages this table for you automatically!

As the video showed, Django abstracts away the complexity of the intermediary table. All you need to do is define the relationship in your model.

This diagram shows the concept:

Django ManyToManyField Illustration
This image shows how a `ManyToManyField` connects two models. We will use the same principle to connect tasks and tags.

Implementing ManyToManyField

To implement this, we first need a model for our tags. Create a new Tag model in tasks/models.py. Then, add the ManyToManyField to your Task model.

Your tasks/models.py file should now look like this:

# tasks/models.py

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

# Add the new Tag model
class Tag(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name

class Task(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField(null=True, blank=True)
    completed = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    due_date = models.DateField(null=True, blank=True)
    user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
    tags = models.ManyToManyField(Tag, blank=True) # Add this line

    def __str__(self):
        return self.title

    class Meta:
        ordering = ['created_at']

We added blank=True to the ManyToManyField to indicate that a task is not required to have a tag.

For a final walkthrough of putting these relationships into code, the following resource provides an excellent, practical example.

Django Tutorial Part 3: Using models

The Mozilla Developer Network (MDN) guide on Django models is a fantastic reference. Let's look at how they build a Book model with relationships to Author and Genre.

Read the sections "Book model" and "BookInstance model". Notice how ForeignKey is used to link a book to a single author, and ManyToManyField is used to link a book to multiple genres. This is a direct parallel to how we're linking a task to a single user and multiple tags.

Apply Your Changes (Again!)

You've now introduced a whole new model (Tag) and a new relationship. Let's get the database up to speed.

  1. Generate the migration file:

    python manage.py makemigrations tasks
    

    You will see output indicating that the Tag model has been created and the tags field has been added to Task.

  2. Apply the migration:

    python manage.py migrate
    

    Django will now create two new tables: tasks_tag (for the Tag model) and tasks_task_tags (the intermediary table to manage the many-to-many relationship).

Test your understanding!

Imagine you want to add a Project model to your application. A single task can only belong to one project, but a single project can contain many tasks.

Which field type would you add to your Task model to represent this relationship, ForeignKey or ManyToManyField?

Show answer

You would use a ForeignKey. The relationship described is "one-to-many" (one project has many tasks). A ForeignKey is placed on the "many" side of the relationship. So, you would add project = models.ForeignKey(Project, ...) to your Task model.


Conclusion

You have just taken a massive step forward in designing your application's data structure. By connecting models, you can represent the complex, real-world relationships that exist in your data. This is the foundation upon which you'll build queries and display interconnected information to your users.

Key Takeaways:

  • One-to-Many Relationships are for when one object can be related to many other objects, but those objects can only relate back to the first one. This is implemented with models.ForeignKey.
  • Many-to-Many Relationships are for when objects from two models can be cross-associated freely. This is implemented with models.ManyToManyField, and Django automatically creates a "join table" to manage it.
  • The on_delete parameter is a critical part of ForeignKey, defining what happens to related objects when one is deleted. models.CASCADE is a common choice.
  • Any change to your models, including adding relationships, requires you to run the makemigrations and migrate workflow.

Preview of the Next Lesson:
Our models are now defined and interconnected. But how do we actually create, read, update, and delete tasks? In the next lesson, we will dive into the Django ORM's query capabilities. You'll learn how to perform these CRUD (Create, Retrieve, Update, Delete) operations, finally allowing you to interact with the database through your Python code.