Skip to content
C Codeloom

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.

Intermediate 9 min read

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.

  • ForeignKey lives on the “many” side and stores a single id of the “one” side.
  • OneToOneField is a ForeignKey with a unique constraint, often used to extend a model.
  • ManyToManyField is 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
Three relationship shapes

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_delete causes a migration error in newer Django versions, but picking CASCADE by reflex can erase real data. Audit each model.
  • Using ManyToManyField when you actually need ordering or extra fields. Promote to a through model early.
  • Iterating posts and accessing post.author in a template without select_related triggers one query per row.
  • Naming reverse accessors poorly. Without related_name, Django invents modelname_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=True on 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=True on the FK and consider SET_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.