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 typeCardinalityTaskFlow example
ForeignKeyMany-to-oneMany Tasks → one Project
ManyToManyFieldMany-to-manyMany Tasks ↔ many Tags
OneToOneFieldOne-to-oneOne User ↔ one UserProfile
Note: All of the code above is correct, real Django ORM usage — but this website's own project doesn't run a live database, so nothing here actually executes on this page.
Next up: Now that TaskFlow's models are properly connected, Aggregation & Annotation shows how to compute summary values — like how many tasks each project has — directly through the ORM.