Difficulty: Beginner
Database relationships are fundamental to modeling real-world data. Django provides three field types for defining relationships between models: ForeignKey for one-to-many, OneToOneField for one-to-one, and ManyToManyField for many-to-many relationships.
ForeignKey defines a one-to-many (or many-to-one) relationship. When you add a ForeignKey field to a model, each instance of that model points to exactly one instance of the related model, but the related model can be pointed to by many instances. For example, many blog posts belong to one author, and one author has many blog posts. In the database, Django creates a column with the suffix '_id' (e.g., author_id) that stores the primary key of the related object.
The on_delete parameter on ForeignKey is required and determines what happens when the referenced object is deleted. CASCADE deletes the related objects (if the author is deleted, their posts are also deleted). PROTECT prevents deletion if related objects exist. SET_NULL sets the foreign key to NULL (requires null=True). SET_DEFAULT sets it to the field's default value. DO_NOTHING does nothing (risky, may cause database integrity errors). In most cases, CASCADE is the right choice, but PROTECT is useful when you want to prevent accidental data loss.
OneToOneField is like ForeignKey with unique=True. It creates a one-to-one relationship where each instance of the model is related to exactly one instance of the related model, and vice versa. Common use cases include extending the User model with a Profile, or splitting a model into two tables for organizational purposes. In the database, it creates a column with a unique constraint.
ManyToManyField defines a many-to-many relationship. Under the hood, Django creates an intermediate junction table (also called a pivot or through table) to manage the relationship. For example, a Student can enroll in many Courses, and a Course can have many Students. You only define ManyToManyField on one of the models - Django handles both sides automatically.
The related_name argument customizes the reverse relationship accessor. By default, Django creates a reverse accessor named 'modelname_set' (e.g., author.post_set.all()). Setting related_name='posts' lets you write author.posts.all() instead, which is more readable. If you have multiple relationships to the same model, related_name is required to avoid naming conflicts.
For complex many-to-many relationships that need extra data on the relationship itself, use a 'through' model. For example, if you want to track when a student enrolled in a course, you create an Enrollment model with extra fields (like enrolled_date and grade) and reference it with the 'through' parameter on ManyToManyField.
Self-referential relationships are also common. A model can have a ForeignKey or ManyToManyField pointing to 'self'. This is useful for tree structures (like comments with replies), social networks (followers), or organizational hierarchies (employees with managers).
from django.db import models
from django.conf import settings
class Category(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='posts',
)
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='posts',
)
# Usage:
# post.author -> returns the User object
# post.category -> returns the Category object
# user.posts.all() -> returns all posts by this user
# category.posts.all() -> returns all posts in this category
author uses CASCADE (delete posts when user is deleted). category uses SET_NULL (keep posts but set category to NULL when category is deleted). related_name='posts' enables clean reverse lookups like user.posts.all().
from django.db import models
from django.conf import settings
class Profile(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='profile',
)
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True)
date_of_birth = models.DateField(null=True, blank=True)
phone = models.CharField(max_length=20, blank=True)
def __str__(self):
return f'Profile of {self.user.username}'
# Usage:
# profile.user -> returns the User object
# user.profile -> returns the Profile object (no _set)
# user.profile.bio -> access profile fields through user
OneToOneField creates a unique relationship. Unlike ForeignKey, the reverse accessor is singular (user.profile, not user.profile_set) because there can only be one profile per user.
from django.db import models
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.name
class Article(models.Model):
title = models.CharField(max_length=200)
tags = models.ManyToManyField(Tag, related_name='articles', blank=True)
def __str__(self):
return self.title
# Usage:
# article.tags.all() -> all tags for this article
# article.tags.add(tag1, tag2) -> add tags to article
# article.tags.remove(tag1) -> remove a tag
# article.tags.clear() -> remove all tags
# tag.articles.all() -> all articles with this tag
# tag.articles.count() -> count of articles
ManyToManyField creates a junction table automatically. You can add, remove, and query related objects from both sides. Only define the field on one model - Django creates the reverse accessor automatically.
from django.db import models
from django.conf import settings
class Course(models.Model):
title = models.CharField(max_length=200)
students = models.ManyToManyField(
settings.AUTH_USER_MODEL,
through='Enrollment',
related_name='courses',
)
class Enrollment(models.Model):
student = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
course = models.ForeignKey(Course, on_delete=models.CASCADE)
enrolled_at = models.DateTimeField(auto_now_add=True)
grade = models.CharField(max_length=2, blank=True)
class Meta:
unique_together = ['student', 'course']
# Usage:
# Enrollment.objects.create(student=user, course=course)
# enrollment.grade = 'A+'
# enrollment.save()
A through model lets you store extra data on the many-to-many relationship itself. Here, Enrollment tracks when a student enrolled and their grade. With through models, you create relationships via the through model, not the M2M field directly.
ForeignKey, OneToOneField, ManyToManyField, on_delete, related_name