Courses / Django Full Stack Web Development
Lesson 6 of 13
Django ORM Relationships: ForeignKey, OneToOne, and ManyToMany
Model real-world relationships in Django with ForeignKey, OneToOneField, and ManyToManyField, including reverse lookups, on_delete, and through tables.
What you'll learn
- ✓When to use ForeignKey vs OneToOne vs ManyToMany
- ✓How reverse relations work
- ✓The role of on_delete
- ✓Through models for extra metadata
- ✓Avoiding N+1 with select_related and prefetch_related
Prerequisites
- •Comfortable with Django models and migrations
What and Why
Relational data is the heart of most web apps. A blog has authors and posts. A store has customers, orders, and products. Django’s ORM gives you three relationship fields that map directly to SQL constraints: ForeignKey (many-to-one), OneToOneField (one-to-one), and ManyToManyField (many-to-many). Picking the right one keeps your schema honest and queries fast.
Mental Model
Think of relationships as arrows between rows.
ForeignKeylives on the “many” side and stores a single id of the “one” side.OneToOneFieldis a ForeignKey with a unique constraint, often used to extend a model.ManyToManyFieldis sugar over a hidden join table connecting both sides.
Django also gives you the reverse direction for free. If Post has author = ForeignKey(User), then user.post_set.all() returns the user’s posts.
ForeignKey (many -> one)
Post.author_id -----> User.id
OneToOneField
Profile.user_id <----> User.id (unique)
ManyToManyField (through join table)
Post <--- PostTag ---> Tag Hands-on Example
Let’s model a simple blog with authors, posts, tags, and profiles.
from django.db import models
from django.contrib.auth.models import User
class Profile(models.Model):
user = models.OneToOneField(
User, on_delete=models.CASCADE, related_name="profile"
)
bio = models.TextField(blank=True)
class Tag(models.Model):
name = models.CharField(max_length=40, unique=True)
class Post(models.Model):
author = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="posts"
)
title = models.CharField(max_length=200)
body = models.TextField()
tags = models.ManyToManyField(Tag, related_name="posts", blank=True)
created_at = models.DateTimeField(auto_now_add=True)
The on_delete argument is required and explains what happens when the referenced row disappears. CASCADE deletes children. PROTECT blocks the delete. SET_NULL requires null=True and nulls the column.
Now some queries:
# Forward access
post = Post.objects.get(pk=1)
print(post.author.username)
print(list(post.tags.all()))
# Reverse access via related_name
user = User.objects.get(username="ada")
print(list(user.posts.all()))
print(user.profile.bio)
# Adding a many-to-many link
post.tags.add(Tag.objects.get(name="python"))
For a many-to-many that needs metadata, like when a tag was applied, switch to a through model:
class PostTag(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE)
tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
added_at = models.DateTimeField(auto_now_add=True)
class Post(models.Model):
tags = models.ManyToManyField(Tag, through="PostTag", related_name="posts")
Finally, batch loads to avoid the N+1 trap when rendering lists:
posts = (
Post.objects
.select_related("author") # join FK in one query
.prefetch_related("tags") # second query, then in-memory join
)
for p in posts:
print(p.author.username, [t.name for t in p.tags.all()])
Common Pitfalls
- Forgetting
on_deletecauses a migration error in newer Django versions, but pickingCASCADEby reflex can erase real data. Audit each model. - Using
ManyToManyFieldwhen you actually need ordering or extra fields. Promote to a through model early. - Iterating
postsand accessingpost.authorin a template withoutselect_relatedtriggers one query per row. - Naming reverse accessors poorly. Without
related_name, Django inventsmodelname_set, which gets ambiguous when two FKs point to the same model.
Practical Tips
- Always set
related_name. It documents intent and keeps reverse queries readable. - Use
db_index=Trueon FKs that you filter on often. Django indexes the column automatically for FKs, but composite filters may need explicit indexes. - Prefer
OneToOneField(parent_link=True)for multi-table inheritance instead of duplicating fields. - When a relationship can be empty, use
null=True, blank=Trueon the FK and considerSET_NULL. - Use
.only()and.defer()to slim down rows when you only need a few columns.
Wrap-up
Relationships are where the ORM earns its keep. Choose the field that matches the cardinality, be explicit about on_delete, and reach for select_related or prefetch_related whenever you render lists. With those habits, your queries stay tight and your models stay honest as the schema grows.
Progress is saved locally to your browser.