ManyToMany & OneToOne
ForeignKey handled the case where many records point at one. But TaskFlow has another
relationship that doesn't fit that shape at all: tags. One Task can carry several
Tags ("urgent", "design", "backend"), and one Tag can be attached to many
different Tasks. Neither side is limited to one match on the other — that's a
many-to-many relationship, and Django models it with ManyToManyField.
Defining the ManyToMany Relationship
Unlike ForeignKey, a ManyToManyField only needs to be declared on one
side of the relationship — Django figures out the other side automatically via related_name:
# taskflow/models.py
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
class Task(models.Model):
title = models.CharField(max_length=200)
# ...other fields from the previous lesson...
tags = models.ManyToManyField(Tag, related_name='tasks', blank=True)
blank=True means a task is allowed to have zero tags — the field isn't required when
creating one. Behind the scenes, Django creates a third, hidden database table just to record which
Task IDs are paired with which Tag IDs — commonly called a
join table. You never define this table yourself and you'll rarely interact with it
directly; the ORM manages it for you every time you use the relationship.
Adding, Removing, and Setting Tags
Because a ManyToManyField holds a collection rather than a single value, you don't assign
to it directly the way you would a ForeignKey. Instead it exposes methods for managing
the collection:
urgent = Tag.objects.create(name='urgent')
design = Tag.objects.create(name='design')
backend = Tag.objects.create(name='backend')
my_task = Task.objects.get(title='Wireframe the homepage')
# add() — attach one or more tags without disturbing existing ones
my_task.tags.add(urgent, design)
# remove() — detach a specific tag
my_task.tags.remove(urgent)
# set() — replace the ENTIRE tag list in one call
my_task.tags.set([design, backend])
.add() and .remove() make targeted changes; .set() wipes out
whatever tags were there before and replaces them wholesale — handy when a form lets a user pick a
fresh list of tags for a task in one submission.
Querying Through a ManyToMany Field
Just like ForeignKey, you can filter across a ManyToManyField using the
double-underscore lookup syntax — Django automatically joins through the hidden table for you:
# Every task tagged "urgent" — joins through the hidden M2M table automatically
Task.objects.filter(tags__name='urgent')
# The reverse direction works too, from a Tag back to its Tasks
Tag.objects.get(name='urgent').tasks.all()
OneToOne: At Most One Match on Each Side
A OneToOneField covers a third shape: each side has at most one matching row on
the other side — not many, like ForeignKey or ManyToManyField allow. None of
TaskFlow's existing models need this yet, but it's the exact pattern used for a very common Django
need: attaching extra profile fields to Django's built-in User model without modifying
User itself.
# A new model, separate from anything TaskFlow already has
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
avatar_url = models.URLField(blank=True)
bio = models.TextField(blank=True)
Each User gets exactly one UserProfile, and each UserProfile
belongs to exactly one User — never more, on either side. Keep this pattern in mind: it
comes back in exactly this form later in Custom User Model,
where it becomes one of the standard ways to extend Django's authentication system.
Comparing All Three Relationship Types
| Field type | Cardinality | TaskFlow example |
|---|---|---|
ForeignKey | Many-to-one | Many Tasks → one Project |
ManyToManyField | Many-to-many | Many Tasks ↔ many Tags |
OneToOneField | One-to-one | One User ↔ one UserProfile |