Relationships: ForeignKey and ManyToMany

Django · Essentials · 14 min

Real data is relational, and following relationships in both directions is everyday ORM work.

Three field types connect models:

  • `ForeignKey` (many-to-one): many posts belong to one author. author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="posts")
  • `ManyToManyField`: posts have many tags and tags many posts. Django creates the join table for you; use through= for a custom one with extra columns.
  • `OneToOneField`: one-to-one extension, such as a Profile for each User.

on_delete is required on ForeignKey and decides what happens when the referenced row is deleted:

  • CASCADE: delete dependent rows too
  • PROTECT / RESTRICT: refuse the deletion with an error
  • SET_NULL: set the column to NULL (requires null=True)
  • SET_DEFAULT, DO_NOTHING

Navigating:

  • forward: post.author (one query the first time, then cached)
  • reverse: author.posts.all(), named by related_name (without it, author.post_set)
  • many-to-many: post.tags.add(t), .remove(t), .set([...]), .clear(), and post.tags.all()

Query across relationships with double underscores, in both directions:

Post.objects.filter(author__name="Ann")
Author.objects.filter(posts__tags__name="django").distinct()

Filters across multi-valued relationships can return duplicates, hence .distinct().

Always set related_name: it makes reverse access readable and avoids clashes when two foreign keys point at the same model.

Example

from django.db import models

class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)

class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey("Author", on_delete=models.CASCADE, related_name="posts")
    tags = models.ManyToManyField(Tag, blank=True, related_name="posts")

class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
    text = models.TextField()

# usage
ann = Author.objects.create(name="Ann")
post = Post.objects.create(title="Hi", author=ann)
post.tags.add(Tag.objects.create(name="django"))
print(post.author.name, ann.posts.count())          # Ann 1
print(Post.objects.filter(author__name="Ann", tags__name="django").count())   # 1
print(Author.objects.filter(posts__comments__isnull=True).distinct().count()) # 1

Remember

  • ForeignKey = many-to-one, ManyToManyField = many-to-many, OneToOneField = one-to-one
  • on_delete decides what deleting the parent does (CASCADE, PROTECT, SET_NULL...)
  • related_name names the reverse accessor; __ spans relationships in queries

Watch out for

  • SET_NULL without null=True on the field, which fails the system check.

Check yourself

  1. Post has author = ForeignKey(Author, related_name="posts", on_delete=PROTECT). What happens when you delete an author with posts?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.djangoproject.com/en/6.0/ref/models/fields/#foreignkey