Introduction
OneToOneField creates a one-to-one relationship where each row in one table links to exactly one row in another table, and vice versa. ForeignKey creates a many-to-one relationship where multiple rows in one table can link to the same row in another table. Under the hood, OneToOneField is a ForeignKey with unique=True, but it changes the reverse accessor from a QuerySet (manager) to a single object. Use OneToOneField for profile extensions, settings, or any "has exactly one" relationship. Use ForeignKey for "belongs to" relationships where many records share the same parent.
OneToOneField Example
1from django.db import models
2from django.contrib.auth.models import User
3
4class UserProfile(models.Model):
5 user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
6 bio = models.TextField(blank=True)
7 avatar = models.ImageField(upload_to='avatars/', null=True)
8 date_of_birth = models.DateField(null=True)
9
10 def __str__(self):
11 return f"Profile of {self.user.username}"
12
13# Usage
14user = User.objects.get(username='alice')
15
16# Forward access: profile → user
17profile = UserProfile.objects.get(user=user)
18print(profile.bio)
19
20# Reverse access: user → profile (returns single object, not QuerySet)
21print(user.profile.bio) # Direct attribute access
22print(user.profile.avatar) # No .all() or .first() needed
The reverse accessor returns a single object directly. Accessing user.profile when no profile exists raises UserProfile.DoesNotExist (a subclass of RelatedObjectDoesNotExist).
ForeignKey Example
1class Author(models.Model):
2 name = models.CharField(max_length=200)
3
4class Book(models.Model):
5 title = models.CharField(max_length=200)
6 author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books')
7 published = models.DateField()
8
9# Usage
10author = Author.objects.get(name='Alice')
11
12# Forward access: book → author (single object)
13book = Book.objects.first()
14print(book.author.name)
15
16# Reverse access: author → books (returns QuerySet manager)
17print(author.books.all()) # QuerySet of all books
18print(author.books.count()) # Number of books
19print(author.books.filter(published__year=2024))
The reverse accessor is a RelatedManager that returns a QuerySet. Multiple books can belong to the same author.
Key Differences
1# OneToOneField — unique constraint enforced
2class UserProfile(models.Model):
3 user = models.OneToOneField(User, on_delete=models.CASCADE)
4
5# Equivalent ForeignKey with unique=True
6class UserProfile(models.Model):
7 user = models.ForeignKey(User, on_delete=models.CASCADE, unique=True)
8 # Works but Django recommends OneToOneField for clarity
9
10# Database level: both create the same schema
11# CREATE TABLE userprofile (
12# id SERIAL PRIMARY KEY,
13# user_id INTEGER UNIQUE REFERENCES auth_user(id)
14# );
| Feature | OneToOneField | ForeignKey |
| Relationship | One-to-one | Many-to-one |
| Reverse accessor | Single object | QuerySet (manager) |
| Database constraint | UNIQUE + FOREIGN KEY | FOREIGN KEY only |
| Reverse syntax | user.profile | author.books.all() |
| DoesNotExist on reverse | Raises exception | Returns empty QuerySet |
When to Use Each
1# OneToOneField — "has exactly one"
2class User(models.Model): ...
3class UserProfile(models.Model): # Each user has exactly one profile
4 user = models.OneToOneField(User, on_delete=models.CASCADE)
5
6class Order(models.Model): ...
7class Invoice(models.Model): # Each order has exactly one invoice
8 order = models.OneToOneField(Order, on_delete=models.CASCADE)
9
10class Car(models.Model): ...
11class Engine(models.Model): # Each car has exactly one engine
12 car = models.OneToOneField(Car, on_delete=models.CASCADE)
13
14# ForeignKey — "belongs to" / "has many"
15class Author(models.Model): ...
16class Book(models.Model): # An author can have many books
17 author = models.ForeignKey(Author, on_delete=models.CASCADE)
18
19class Category(models.Model): ...
20class Product(models.Model): # A category has many products
21 category = models.ForeignKey(Category, on_delete=models.CASCADE)
22
23class User(models.Model): ...
24class Comment(models.Model): # A user can post many comments
25 user = models.ForeignKey(User, on_delete=models.CASCADE)
Handling the Reverse Accessor
1# OneToOneField reverse — may not exist
2user = User.objects.get(username='alice')
3
4# Safe access pattern
5try:
6 profile = user.profile
7except UserProfile.DoesNotExist:
8 profile = UserProfile.objects.create(user=user)
9
10# Or use hasattr
11if hasattr(user, 'profile'):
12 print(user.profile.bio)
13
14# Or select_related to avoid extra query
15user = User.objects.select_related('profile').get(username='alice')
16
17# ForeignKey reverse — always returns a manager
18author = Author.objects.get(name='Alice')
19books = author.books.all() # Always works, may be empty QuerySet
20latest = author.books.order_by('-published').first() # None if no books
21
22# Prefetch related for ForeignKey reverse
23authors = Author.objects.prefetch_related('books').all()
24for author in authors:
25 print(author.books.count()) # No additional queries
on_delete Options
1# CASCADE — delete related objects when parent is deleted
2profile = models.OneToOneField(User, on_delete=models.CASCADE)
3
4# PROTECT — prevent deletion if related objects exist
5profile = models.OneToOneField(User, on_delete=models.PROTECT)
6
7# SET_NULL — set to NULL (requires null=True)
8profile = models.OneToOneField(User, on_delete=models.SET_NULL, null=True)
9
10# SET_DEFAULT — set to default value
11author = models.ForeignKey(Author, on_delete=models.SET_DEFAULT, default=1)
12
13# DO_NOTHING — no action (may break referential integrity)
14author = models.ForeignKey(Author, on_delete=models.DO_NOTHING)
Common Pitfalls
Using ForeignKey where OneToOneField is needed: If a profile should be unique per user, a ForeignKey allows multiple profiles per user (violating the design intent). Use OneToOneField to enforce the constraint at the database level, not just in application logic.
Not handling DoesNotExist on reverse OneToOneField access: user.profile raises RelatedObjectDoesNotExist if no profile exists. Always wrap in try/except or use hasattr(user, 'profile'). ForeignKey reverse access (author.books.all()) never raises — it returns an empty QuerySet.
Forgetting select_related for OneToOneField queries: Each user.profile access executes a separate SQL query. Use User.objects.select_related('profile') to fetch both in a single JOIN query, especially in loops or templates.
Using related_name inconsistently: Without related_name, Django generates a default name (userprofile for OneToOneField, book_set for ForeignKey). Specify related_name explicitly for clarity and to avoid clashes when multiple foreign keys point to the same model.
Confusing on_delete behavior: on_delete=models.CASCADE on a OneToOneField deletes the profile when the user is deleted. If you want profiles to survive user deletion, use SET_NULL with null=True. The on_delete argument is required and has no default — Django forces you to choose.
Summary
OneToOneField enforces a unique one-to-one relationship; ForeignKey allows many-to-one
Reverse access differs: user.profile returns a single object; author.books.all() returns a QuerySet
OneToOneField is equivalent to ForeignKey(unique=True) at the database level
Use select_related for OneToOneField and prefetch_related for ForeignKey to optimize queries
Handle DoesNotExist on reverse OneToOneField access — it raises an exception if the related object is missing
Always specify on_delete and related_name explicitly for clarity