backend / web frameworks / django / django orm / 03_django_relationships_comprehensive_guide.md

Django ORM Relationships: One-to-Many, Many-to-Many, One-to-One

3 interview angles 9 min read source

Django ORM Relationships: One-to-Many, Many-to-Many, One-to-One

Overview

Django ORM provides three main types of relationships between models: One-to-Many, Many-to-Many, and One-to-One. Understanding these relationships is crucial for designing efficient database schemas and writing effective Django applications.

Table of Contents

  1. One-to-Many Relationship
  2. Many-to-Many Relationship
  3. One-to-One Relationship
  4. Comparison Table
  5. Best Practices
  6. Common Interview Questions
  7. Performance Considerations
  8. Real-World Examples

One-to-Many Relationship

Definition

A One-to-Many relationship occurs when one record in a table can be associated with multiple records in another table, but each record in the second table can only be associated with one record in the first table.

Django Implementation

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField()
    bio = models.TextField()
    
    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    publication_date = models.DateField()
    isbn = models.CharField(max_length=13)
    
    def __str__(self):
        return self.title

Key Characteristics

  • Foreign Key: Uses ForeignKey field
  • Direction: One-to-Many (one author can have many books)
  • Database: Creates a foreign key column in the “many” table
  • Access: Bidirectional access through Django’s reverse relationships

Querying Examples

# Get all books by an author
author = Author.objects.get(name="J.K. Rowling")
books = author.book_set.all()  # Reverse relationship

# Get the author of a book
book = Book.objects.get(title="Harry Potter")
author = book.author  # Direct relationship

# Get all authors with their book counts
authors_with_book_count = Author.objects.annotate(
    book_count=Count('book')
)

# Get books published after 2000
recent_books = Book.objects.filter(publication_date__year__gt=2000)

Database Schema

-- Authors table
CREATE TABLE app_author (
    id INTEGER PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(254),
    bio TEXT
);

-- Books table with foreign key
CREATE TABLE app_book (
    id INTEGER PRIMARY KEY,
    title VARCHAR(200),
    author_id INTEGER,  -- Foreign key column
    publication_date DATE,
    isbn VARCHAR(13),
    FOREIGN KEY (author_id) REFERENCES app_author(id)
);

Use Cases

  • Blog Posts and Comments: One post can have many comments
  • User and Orders: One user can have many orders
  • Category and Products: One category can have many products
  • Department and Employees: One department can have many employees

Many-to-Many Relationship

Definition

A Many-to-Many relationship occurs when multiple records in one table can be associated with multiple records in another table.

Django Implementation

from django.db import models

class Student(models.Model):
    name = models.CharField(max_length=100)
    student_id = models.CharField(max_length=20, unique=True)
    email = models.EmailField()
    
    def __str__(self):
        return self.name

class Course(models.Model):
    title = models.CharField(max_length=200)
    code = models.CharField(max_length=10, unique=True)
    students = models.ManyToManyField(Student, through='Enrollment')
    
    def __str__(self):
        return self.title

class Enrollment(models.Model):
    student = models.ForeignKey(Student, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, on_delete=models.CASCADE)
    enrollment_date = models.DateField(auto_now_add=True)
    grade = models.CharField(max_length=2, blank=True, null=True)
    
    class Meta:
        unique_together = ['student', 'course']

Key Characteristics

  • ManyToManyField: Uses ManyToManyField or through model
  • Direction: Many-to-Many (many students can take many courses)
  • Database: Creates an intermediate/junction table
  • Access: Bidirectional access through both models

Querying Examples

# Get all courses for a student
student = Student.objects.get(name="Alice")
courses = student.course_set.all()

# Get all students in a course
course = Course.objects.get(code="CS101")
students = course.students.all()

# Get students with their course counts
students_with_course_count = Student.objects.annotate(
    course_count=Count('course')
)

# Get courses with student counts
courses_with_student_count = Course.objects.annotate(
    student_count=Count('students')
)

# Using through model for additional data
enrollments = Enrollment.objects.filter(grade='A')

Database Schema

-- Students table
CREATE TABLE app_student (
    id INTEGER PRIMARY KEY,
    name VARCHAR(100),
    student_id VARCHAR(20) UNIQUE,
    email VARCHAR(254)
);

-- Courses table
CREATE TABLE app_course (
    id INTEGER PRIMARY KEY,
    title VARCHAR(200),
    code VARCHAR(10) UNIQUE
);

-- Junction table (Enrollment)
CREATE TABLE app_enrollment (
    id INTEGER PRIMARY KEY,
    student_id INTEGER,
    course_id INTEGER,
    enrollment_date DATE,
    grade VARCHAR(2),
    FOREIGN KEY (student_id) REFERENCES app_student(id),
    FOREIGN KEY (course_id) REFERENCES app_course(id),
    UNIQUE(student_id, course_id)
);

Use Cases

  • Users and Groups: Many users can belong to many groups
  • Tags and Posts: Many posts can have many tags
  • Students and Courses: Many students can take many courses
  • Products and Categories: Many products can belong to many categories

One-to-One Relationship

Definition

A One-to-One relationship occurs when one record in a table can be associated with exactly one record in another table, and vice versa.

Django Implementation

from django.db import models
from django.contrib.auth.models import User

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True)
    birth_date = models.DateField(null=True, blank=True)
    phone_number = models.CharField(max_length=15, blank=True)
    profile_picture = models.ImageField(upload_to='profile_pics/', blank=True)
    
    def __str__(self):
        return f"{self.user.username}'s profile"

class User(models.Model):
    # Django's built-in User model
    username = models.CharField(max_length=150, unique=True)
    email = models.EmailField()
    # ... other fields

Key Characteristics

  • OneToOneField: Uses OneToOneField
  • Direction: One-to-One (one user has exactly one profile)
  • Database: Creates a foreign key with unique constraint
  • Access: Direct access from both sides

Querying Examples

# Get user's profile
user = User.objects.get(username="john_doe")
profile = user.userprofile  # Direct access

# Get profile's user
profile = UserProfile.objects.get(id=1)
user = profile.user  # Direct access

# Get all users with profiles
users_with_profiles = User.objects.filter(userprofile__isnull=False)

# Get profiles with specific criteria
profiles_with_phone = UserProfile.objects.filter(phone_number__isnull=False)

Database Schema

-- Users table (Django's built-in)
CREATE TABLE auth_user (
    id INTEGER PRIMARY KEY,
    username VARCHAR(150) UNIQUE,
    email VARCHAR(254),
    -- ... other fields
);

-- UserProfile table with unique foreign key
CREATE TABLE app_userprofile (
    id INTEGER PRIMARY KEY,
    user_id INTEGER UNIQUE,  -- Unique constraint
    bio TEXT,
    birth_date DATE,
    phone_number VARCHAR(15),
    profile_picture VARCHAR(100),
    FOREIGN KEY (user_id) REFERENCES auth_user(id)
);

Use Cases

  • User and Profile: One user has one profile
  • Product and Inventory: One product has one inventory record
  • Employee and EmployeeDetails: One employee has one detail record
  • Order and Shipping: One order has one shipping information

Comparison Table

Aspect One-to-Many Many-to-Many One-to-One
Django Field ForeignKey ManyToManyField OneToOneField
Database Structure Foreign key in “many” table Junction table Foreign key with unique constraint
Relationship 1:N M:N 1:1
Reverse Access model_set model_set Direct access
Use Cases Blog posts & comments Users & groups User & profile
Performance Good Can be complex Excellent
Complexity Low Medium Low

Best Practices

1. One-to-Many Relationships

# Good: Use related_name for clarity
class Comment(models.Model):
    post = models.ForeignKey(
        Post, 
        on_delete=models.CASCADE,
        related_name='comments'
    )
    # Now use: post.comments.all() instead of post.comment_set.all()

# Good: Use on_delete appropriately
class Order(models.Model):
    customer = models.ForeignKey(
        Customer, 
        on_delete=models.PROTECT  # Prevent customer deletion if orders exist
    )

2. Many-to-Many Relationships

# Good: Use through model for additional data
class Recipe(models.Model):
    ingredients = models.ManyToManyField(
        Ingredient, 
        through='RecipeIngredient'
    )

class RecipeIngredient(models.Model):
    recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
    ingredient = models.ForeignKey(Ingredient, on_delete=models.CASCADE)
    amount = models.DecimalField(max_digits=5, decimal_places=2)
    unit = models.CharField(max_length=20)

3. One-to-One Relationships

# Good: Use for extending existing models
class UserProfile(models.Model):
    user = models.OneToOneField(
        User, 
        on_delete=models.CASCADE,
        related_name='profile'
    )
    # Access with: user.profile instead of user.userprofile

4. General Best Practices

# Good: Use select_related for ForeignKey
posts = Post.objects.select_related('author').all()

# Good: Use prefetch_related for ManyToManyField
posts = Post.objects.prefetch_related('tags').all()

# Good: Use related_name for clarity
class Post(models.Model):
    author = models.ForeignKey(
        Author, 
        on_delete=models.CASCADE,
        related_name='posts'
    )
    tags = models.ManyToManyField(
        Tag, 
        related_name='posts'
    )

Common Interview Questions

1. Basic Questions

Q: What’s the difference between ForeignKey and OneToOneField?

# ForeignKey: One-to-Many
class Book(models.Model):
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    # One author can have many books

# OneToOneField: One-to-One
class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    # One user has exactly one profile

Q: How do you create a Many-to-Many relationship?

# Simple Many-to-Many
class Post(models.Model):
    tags = models.ManyToManyField(Tag)

# Many-to-Many with additional data
class Post(models.Model):
    tags = models.ManyToManyField(Tag, through='PostTag')

class PostTag(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
    added_date = models.DateTimeField(auto_now_add=True)

2. Advanced Questions

Q: How do you optimize queries with relationships?

# Bad: N+1 query problem
posts = Post.objects.all()
for post in posts:
    print(post.author.name)  # Additional query for each post

# Good: Use select_related
posts = Post.objects.select_related('author').all()
for post in posts:
    print(post.author.name)  # No additional queries

# Good: Use prefetch_related for ManyToMany
posts = Post.objects.prefetch_related('tags').all()
for post in posts:
    print(post.tags.all())  # No additional queries

Q: How do you handle cascading deletes?

# CASCADE: Delete related objects
class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    # When post is deleted, comments are also deleted

# PROTECT: Prevent deletion if related objects exist
class Order(models.Model):
    customer = models.ForeignKey(Customer, on_delete=models.PROTECT)
    # Cannot delete customer if orders exist

# SET_NULL: Set foreign key to NULL
class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.SET_NULL, null=True)
    # When post is deleted, comment.post becomes NULL

3. Practical Questions

Q: How do you implement a tagging system?

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    tags = models.ManyToManyField(Tag, blank=True)

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

# Usage
post = Post.objects.create(title="Django Tutorial", content="...")
tag = Tag.objects.get_or_create(name="python")[0]
post.tags.add(tag)

# Query posts by tag
python_posts = Post.objects.filter(tags__name="python")

Q: How do you implement user profiles?

from django.contrib.auth.models import User

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True)
    birth_date = models.DateField(null=True, blank=True)
    
    def __str__(self):
        return f"{self.user.username}'s profile"

# Create profile when user is created
from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        UserProfile.objects.create(user=instance)

Performance Considerations

1. Query Optimization

# Use select_related for ForeignKey relationships
posts = Post.objects.select_related('author', 'category').all()

# Use prefetch_related for ManyToMany relationships
posts = Post.objects.prefetch_related('tags', 'comments').all()

# Use prefetch_related with Prefetch for complex queries
from django.db.models import Prefetch

posts = Post.objects.prefetch_related(
    Prefetch('comments', queryset=Comment.objects.filter(is_approved=True))
).all()

2. Database Indexes

class Order(models.Model):
    customer = models.ForeignKey(
        Customer, 
        on_delete=models.CASCADE,
        db_index=True  # Add database index
    )
    order_date = models.DateTimeField(db_index=True)

3. Lazy Loading vs Eager Loading

# Lazy loading (default)
post = Post.objects.get(id=1)
author = post.author  # Additional query

# Eager loading
post = Post.objects.select_related('author').get(id=1)
author = post.author  # No additional query

Real-World Examples

1. E-commerce System

class Category(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField()

class Product(models.Model):
    name = models.CharField(max_length=200)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)  # One-to-Many
    tags = models.ManyToManyField('Tag')  # Many-to-Many
    price = models.DecimalField(max_digits=10, decimal_places=2)

class ProductDetail(models.Model):
    product = models.OneToOneField(Product, on_delete=models.CASCADE)  # One-to-One
    specifications = models.JSONField()
    warranty_info = models.TextField()

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

2. Blog System

class Author(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField()

class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)  # One-to-Many
    tags = models.ManyToManyField('Tag')  # Many-to-Many
    content = models.TextField()

class PostMeta(models.Model):
    post = models.OneToOneField(Post, on_delete=models.CASCADE)  # One-to-One
    view_count = models.IntegerField(default=0)
    last_updated = models.DateTimeField(auto_now=True)

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

3. Social Media System

class User(models.Model):
    username = models.CharField(max_length=100, unique=True)
    email = models.EmailField()

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)  # One-to-One
    bio = models.TextField()
    avatar = models.ImageField(upload_to='avatars/')

class Post(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)  # One-to-Many
    content = models.TextField()
    likes = models.ManyToManyField(User, related_name='liked_posts')  # Many-to-Many
    created_at = models.DateTimeField(auto_now_add=True)

Summary

Understanding Django ORM relationships is essential for building efficient and maintainable applications:

  • One-to-Many: Use ForeignKey for hierarchical relationships
  • Many-to-Many: Use ManyToManyField for complex associations
  • One-to-One: Use OneToOneField for extending existing models

Each relationship type has its own use cases, performance characteristics, and best practices. Choose the appropriate relationship based on your data model requirements and consider performance implications when designing your database schema.

Interview angle

  • “Which relationship field for which shape?” - ForeignKey for many-to-one, OneToOneField for one-to-one (profile extensions), ManyToManyField for many-to-many. Use through when the relationship itself carries data such as a joined-at timestamp.
  • “What does on_delete control, and what’s the safe default?” - what happens to this row when the referenced row is deleted. CASCADE deletes children, PROTECT blocks the delete, SET_NULL clears the reference. PROTECT is the safer default for anything financial - accidental cascades are hard to undo.
  • “What’s related_name for?” - naming the reverse accessor. Setting it deliberately makes reverse queries readable and avoids clashes when a model has two foreign keys to the same target.