ForeignKey Relationships

Every TaskFlow task belongs to exactly one project, and a project can naturally hold many tasks. That shape — one record on one side, many records pointing back at it — is called a one-to-many relationship, and it's by far the most common kind of relationship you'll model in Django. The tool for it is ForeignKey.

Defining the Relationship

A ForeignKey is defined on the "many" side of the relationship — here, that's Task, since many tasks can belong to one project. The field points at the model on the "one" side:

# taskflow/models.py
class Project(models.Model):
    name = models.CharField(max_length=150)
    description = models.TextField(blank=True)
    owner = models.ForeignKey(User, on_delete=models.CASCADE)

class Task(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    due_date = models.DateField(null=True, blank=True)
    priority = models.CharField(max_length=10)
    completed = models.BooleanField(default=False)
    project = models.ForeignKey(
        Project,
        on_delete=models.CASCADE,
        related_name='tasks'
    )

Notice Project itself has a ForeignKey too — owner, pointing at Django's built-in User model. One user can own many projects, so it's the same one-to-many shape again. A model can have as many ForeignKey fields as it needs relationships.

The on_delete Argument

on_delete is not optional — Django requires you to say explicitly what should happen to a Task if the Project it points to gets deleted. This one decision has real consequences for your data, so it's worth understanding the common choices:

OptionBehavior when the related row is deleted
models.CASCADEDeletes the tasks too — if a Project is removed, every Task pointing at it goes with it. The most common choice for "this child record has no meaning without its parent."
models.PROTECTRefuses the deletion outright — Django raises a ProtectedError and blocks removing the Project while it still has Tasks attached, forcing you to handle those tasks first.
models.SET_NULLSets the foreign key to NULL instead of deleting the related row. Requires the field to also declare null=True, since the column must be allowed to hold no value.

For TaskFlow, CASCADE makes sense on Task.project — a task genuinely doesn't belong anywhere once its project is gone. If you wanted tasks to survive a deleted project instead (say, to move them into an "unassigned" bucket), you'd use SET_NULL with null=True on the project field.

related_name: Querying Backwards

A ForeignKey gives you an obvious forward path — from a Task to its Project. But Django also automatically creates a reverse path, from a Project back to all the Tasks that point at it. related_name is what names that reverse path.

Without related_name, Django falls back to a default reverse accessor built from the model's name in lowercase, plus _set — for Task, that would be my_project.task_set.all(). It works, but it reads awkwardly and doesn't say anything about what the relationship actually means. Setting related_name='tasks' replaces that with something that reads naturally:

# Forward: from a Task to its Project
my_task.project.name

# Reverse, WITH related_name='tasks' set on the ForeignKey:
my_project.tasks.all()

# Reverse, WITHOUT related_name (the awkward Django default):
my_project.task_set.all()

related_name costs nothing to add and makes every reverse query in your codebase read like plain English — it's worth setting deliberately on every ForeignKey you define.

A Full Worked Example: Both Directions

Putting it together — creating a project, attaching a few tasks to it, then reading the relationship from both sides:

# Create a project owned by the current user
redesign = Project.objects.create(
    name='Website Redesign',
    description='Refresh the marketing site',
    owner=request.user
)

# Create tasks linked to it via the project ForeignKey
Task.objects.create(title='Wireframe the homepage', priority='high', project=redesign)
Task.objects.create(title='Pick a new color palette', priority='medium', project=redesign)
Task.objects.create(title='Write updated copy', priority='medium', project=redesign)

# FORWARD: from a task, find its project
first_task = Task.objects.first()
print(first_task.project.name)               # 'Website Redesign'

# REVERSE: from the project, find all of its tasks
print(redesign.tasks.count())                # 3
for t in redesign.tasks.all():
    print(t.title)

# The reverse relationship is a full QuerySet, so it chains with everything
# you've already learned — filtering, ordering, and so on:
redesign.tasks.filter(completed=False).order_by('due_date')
Note: This is correct, real ORM code — but this website's own project doesn't run a live database, so it isn't executed on this page. Try it in a real TaskFlow project to see the actual rows come back.
Next up: ForeignKey covers "one project, many tasks." ManyToMany & OneToOne covers the other two relationship shapes — tasks that can each have many tags, and a tag that can apply to many tasks.