Django's ORM provides powerful tools for defining and working with relationships between models. Understanding how to properly design and implement relationships is crucial for creating efficient, maintainable database schemas.
One-to-one relationships link exactly one record in one table to exactly one record in another table. This is useful for extending models or separating concerns.
# models.py
from django.db import models
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
class UserProfile(models.Model):
"""Extends the built-in User model with additional fields"""
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name='profile'
)
# Additional profile fields
bio = models.TextField(max_length=500, blank=True)
birth_date = models.DateField(null=True, blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
phone = models.CharField(max_length=20, blank=True)
website = models.URLField(blank=True)
# Social media links
twitter_handle = models.CharField(max_length=50, blank=True)
linkedin_url = models.URLField(blank=True)
# Preferences
email_notifications = models.BooleanField(default=True)
newsletter_subscription = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.user.username}'s Profile"
@property
def full_name(self):
return self.user.get_full_name() or self.user.username
@property
def age(self):
if self.birth_date:
from django.utils import timezone
today = timezone.now().date()
return today.year - self.birth_date.year - (
(today.month, today.day) < (self.birth_date.month, self.birth_date.day)
)
return None
# Signal to automatically 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)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
if hasattr(instance, 'profile'):
instance.profile.save()
# Usage examples:
# user = User.objects.get(username='john')
# profile = user.profile # Access via related_name
# user_from_profile = profile.user # Reverse access
# models.py
from django.db import models
class Company(models.Model):
name = models.CharField(max_length=200)
founded_date = models.DateField()
def __str__(self):
return self.name
class CompanySettings(models.Model):
"""Company-specific configuration settings"""
company = models.OneToOneField(
Company,
on_delete=models.CASCADE,
related_name='settings'
)
# Business settings
business_hours_start = models.TimeField(default='09:00')
business_hours_end = models.TimeField(default='17:00')
timezone = models.CharField(max_length=50, default='UTC')
# Notification settings
email_notifications = models.BooleanField(default=True)
sms_notifications = models.BooleanField(default=False)
# API settings
api_rate_limit = models.PositiveIntegerField(default=1000)
api_key = models.CharField(max_length=64, unique=True, editable=False)
def save(self, *args, **kwargs):
if not self.api_key:
import secrets
self.api_key = secrets.token_hex(32)
super().save(*args, **kwargs)
class Employee(models.Model):
company = models.ForeignKey(Company, on_delete=models.CASCADE)
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
email = models.EmailField(unique=True)
def __str__(self):
return f"{self.first_name} {self.last_name}"
class EmployeeDetails(models.Model):
"""Extended employee information"""
employee = models.OneToOneField(
Employee,
on_delete=models.CASCADE,
related_name='details'
)
# Personal details
employee_id = models.CharField(max_length=20, unique=True)
hire_date = models.DateField()
department = models.CharField(max_length=100)
position = models.CharField(max_length=100)
salary = models.DecimalField(max_digits=10, decimal_places=2)
# Emergency contact
emergency_contact_name = models.CharField(max_length=100)
emergency_contact_phone = models.CharField(max_length=20)
# Benefits
health_insurance = models.BooleanField(default=True)
dental_insurance = models.BooleanField(default=False)
retirement_plan = models.BooleanField(default=True)
def __str__(self):
return f"Details for {self.employee}"
One-to-many relationships are the most common type of relationship, where one record can be related to multiple records in another table.
# models.py
from django.db import models
from django.contrib.auth.models import User
class Category(models.Model):
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(max_length=100, unique=True)
description = models.TextField(blank=True)
parent = models.ForeignKey(
'self', # Self-referential foreign key
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='subcategories'
)
class Meta:
verbose_name_plural = 'Categories'
ordering = ['name']
def __str__(self):
return self.name
@property
def is_root_category(self):
return self.parent is None
def get_ancestors(self):
"""Get all parent categories"""
ancestors = []
current = self.parent
while current:
ancestors.append(current)
current = current.parent
return ancestors
def get_descendants(self):
"""Get all child categories recursively"""
descendants = []
for child in self.subcategories.all():
descendants.append(child)
descendants.extend(child.get_descendants())
return descendants
class Post(models.Model):
STATUS_CHOICES = [
('draft', 'Draft'),
('published', 'Published'),
('archived', 'Archived'),
]
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique=True)
# Foreign key to User model
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='posts' # Allows user.posts.all()
)
# Foreign key to Category model
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL, # Keep posts if category is deleted
null=True,
blank=True,
related_name='posts'
)
content = models.TextField()
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
published_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['status', 'published_at']),
models.Index(fields=['author', 'status']),
models.Index(fields=['category', 'status']),
]
def __str__(self):
return self.title
class Comment(models.Model):
# Foreign key to Post
post = models.ForeignKey(
Post,
on_delete=models.CASCADE,
related_name='comments'
)
# Foreign key to User
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='comments'
)
# Self-referential foreign key for nested comments
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='replies'
)
content = models.TextField()
is_approved = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['created_at']
def __str__(self):
return f'Comment by {self.author.username} on {self.post.title}'
@property
def is_reply(self):
return self.parent is not None
def get_replies(self):
"""Get all replies to this comment"""
return self.replies.filter(is_approved=True)
# Usage examples:
# Get all posts by an author
# author_posts = user.posts.all()
# Get all posts in a category
# category_posts = category.posts.filter(status='published')
# Get all comments for a post
# post_comments = post.comments.filter(is_approved=True)
# Get all replies to a comment
# comment_replies = comment.replies.all()
# models.py
from django.db import models
class Publisher(models.Model):
name = models.CharField(max_length=200)
address = models.TextField()
def __str__(self):
return self.name
class Author(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField()
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=200)
isbn = models.CharField(max_length=13, unique=True)
# CASCADE: Delete books when publisher is deleted
publisher = models.ForeignKey(
Publisher,
on_delete=models.CASCADE,
related_name='books'
)
# SET_NULL: Keep books but set author to NULL when author is deleted
author = models.ForeignKey(
Author,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='books'
)
# PROTECT: Prevent deletion of referenced object
primary_category = models.ForeignKey(
'Category',
on_delete=models.PROTECT,
related_name='primary_books'
)
# SET_DEFAULT: Set to default value when referenced object is deleted
backup_publisher = models.ForeignKey(
Publisher,
on_delete=models.SET_DEFAULT,
default=1, # Must have a publisher with id=1
related_name='backup_books'
)
publication_date = models.DateField()
pages = models.PositiveIntegerField()
def __str__(self):
return self.title
# Custom deletion behavior
def get_default_author():
"""Get or create a default 'Unknown Author'"""
author, created = Author.objects.get_or_create(
name='Unknown Author',
defaults={'email': 'unknown@example.com'}
)
return author.pk
class Article(models.Model):
title = models.CharField(max_length=200)
# SET: Call a function when referenced object is deleted
author = models.ForeignKey(
Author,
on_delete=models.SET(get_default_author),
related_name='articles'
)
content = models.TextField()
def __str__(self):
return self.title
class Order(models.Model):
customer = models.ForeignKey(
'Customer',
on_delete=models.CASCADE,
related_name='orders'
)
order_date = models.DateTimeField(auto_now_add=True)
total_amount = models.DecimalField(max_digits=10, decimal_places=2)
def __str__(self):
return f'Order #{self.pk}'
class OrderItem(models.Model):
order = models.ForeignKey(
Order,
on_delete=models.CASCADE,
related_name='items'
)
product = models.ForeignKey(
'Product',
on_delete=models.CASCADE,
related_name='order_items'
)
quantity = models.PositiveIntegerField()
unit_price = models.DecimalField(max_digits=8, decimal_places=2)
@property
def total_price(self):
return self.quantity * self.unit_price
def __str__(self):
return f'{self.quantity}x {self.product.name}'
Many-to-many relationships allow multiple records in one table to be related to multiple records in another table.
# models.py
from django.db import models
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(max_length=50, unique=True)
color = models.CharField(max_length=7, default='#007bff') # Hex color
def __str__(self):
return self.name
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
# Simple many-to-many relationship
tags = models.ManyToManyField(
Tag,
blank=True,
related_name='articles',
help_text='Select relevant tags'
)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
def get_tag_names(self):
"""Get comma-separated list of tag names"""
return ', '.join(tag.name for tag in self.tags.all())
# Usage examples:
# Add tags to an article
# article = Article.objects.get(id=1)
# tag1 = Tag.objects.get(name='Django')
# tag2 = Tag.objects.get(name='Python')
# article.tags.add(tag1, tag2)
# Remove tags
# article.tags.remove(tag1)
# Set tags (replaces all existing tags)
# article.tags.set([tag1, tag2])
# Get all articles with a specific tag
# django_articles = Tag.objects.get(name='Django').articles.all()
# Get articles with multiple tags
# articles_with_both_tags = Article.objects.filter(
# tags__name='Django'
# ).filter(tags__name='Python')
# models.py
from django.db import models
from django.contrib.auth.models import User
class Project(models.Model):
name = models.CharField(max_length=200)
description = models.TextField()
start_date = models.DateField()
end_date = models.DateField(null=True, blank=True)
def __str__(self):
return self.name
class Skill(models.Model):
name = models.CharField(max_length=100, unique=True)
category = models.CharField(max_length=50)
def __str__(self):
return self.name
class Person(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(blank=True)
# Many-to-many with through model
projects = models.ManyToManyField(
Project,
through='ProjectMembership',
related_name='members'
)
skills = models.ManyToManyField(
Skill,
through='PersonSkill',
related_name='people'
)
def __str__(self):
return self.user.get_full_name() or self.user.username
class ProjectMembership(models.Model):
"""Through model for Person-Project relationship"""
ROLE_CHOICES = [
('manager', 'Project Manager'),
('developer', 'Developer'),
('designer', 'Designer'),
('tester', 'Tester'),
('analyst', 'Business Analyst'),
]
person = models.ForeignKey(Person, on_delete=models.CASCADE)
project = models.ForeignKey(Project, on_delete=models.CASCADE)
role = models.CharField(max_length=20, choices=ROLE_CHOICES)
start_date = models.DateField()
end_date = models.DateField(null=True, blank=True)
hours_per_week = models.PositiveSmallIntegerField(default=40)
is_active = models.BooleanField(default=True)
class Meta:
unique_together = [['person', 'project', 'role']]
ordering = ['start_date']
def __str__(self):
return f'{self.person} - {self.project} ({self.role})'
@property
def is_current(self):
from django.utils import timezone
today = timezone.now().date()
return (
self.is_active and
self.start_date <= today and
(self.end_date is None or self.end_date >= today)
)
class PersonSkill(models.Model):
"""Through model for Person-Skill relationship"""
PROFICIENCY_CHOICES = [
(1, 'Beginner'),
(2, 'Intermediate'),
(3, 'Advanced'),
(4, 'Expert'),
]
person = models.ForeignKey(Person, on_delete=models.CASCADE)
skill = models.ForeignKey(Skill, on_delete=models.CASCADE)
proficiency_level = models.PositiveSmallIntegerField(choices=PROFICIENCY_CHOICES)
years_of_experience = models.PositiveSmallIntegerField(default=0)
is_certified = models.BooleanField(default=False)
acquired_date = models.DateField()
class Meta:
unique_together = [['person', 'skill']]
def __str__(self):
return f'{self.person} - {self.skill} ({self.get_proficiency_level_display()})'
# Usage with through models:
# Create membership
# membership = ProjectMembership.objects.create(
# person=person,
# project=project,
# role='developer',
# start_date='2023-01-01',
# hours_per_week=40
# )
# Query through the relationship
# current_memberships = ProjectMembership.objects.filter(
# person=person,
# is_active=True
# )
# Get all developers on a project
# developers = ProjectMembership.objects.filter(
# project=project,
# role='developer',
# is_active=True
# )
# models.py
from django.db import models
class Course(models.Model):
name = models.CharField(max_length=200)
code = models.CharField(max_length=10, unique=True)
credits = models.PositiveSmallIntegerField()
def __str__(self):
return f'{self.code}: {self.name}'
class Student(models.Model):
student_id = models.CharField(max_length=20, unique=True)
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
email = models.EmailField(unique=True)
# Many-to-many with additional enrollment data
courses = models.ManyToManyField(
Course,
through='Enrollment',
related_name='students'
)
def __str__(self):
return f'{self.first_name} {self.last_name} ({self.student_id})'
def get_current_courses(self):
"""Get currently enrolled courses"""
from django.utils import timezone
current_semester = timezone.now().year # Simplified
return self.courses.filter(
enrollment__semester_year=current_semester,
enrollment__is_active=True
)
def get_gpa(self):
"""Calculate GPA from completed courses"""
completed_enrollments = Enrollment.objects.filter(
student=self,
grade__isnull=False
)
if not completed_enrollments:
return None
total_points = 0
total_credits = 0
grade_points = {'A': 4.0, 'B': 3.0, 'C': 2.0, 'D': 1.0, 'F': 0.0}
for enrollment in completed_enrollments:
points = grade_points.get(enrollment.grade, 0.0)
credits = enrollment.course.credits
total_points += points * credits
total_credits += credits
return total_points / total_credits if total_credits > 0 else 0.0
class Enrollment(models.Model):
"""Through model for Student-Course relationship"""
GRADE_CHOICES = [
('A', 'A'),
('B', 'B'),
('C', 'C'),
('D', 'D'),
('F', 'F'),
('I', 'Incomplete'),
('W', 'Withdrawn'),
]
student = models.ForeignKey(Student, on_delete=models.CASCADE)
course = models.ForeignKey(Course, on_delete=models.CASCADE)
# Enrollment details
semester_year = models.PositiveIntegerField()
semester_term = models.CharField(
max_length=10,
choices=[('fall', 'Fall'), ('spring', 'Spring'), ('summer', 'Summer')]
)
enrollment_date = models.DateField(auto_now_add=True)
is_active = models.BooleanField(default=True)
# Grading
grade = models.CharField(max_length=2, choices=GRADE_CHOICES, null=True, blank=True)
grade_date = models.DateField(null=True, blank=True)
# Attendance tracking
classes_attended = models.PositiveIntegerField(default=0)
total_classes = models.PositiveIntegerField(default=0)
class Meta:
unique_together = [['student', 'course', 'semester_year', 'semester_term']]
ordering = ['-semester_year', 'semester_term']
def __str__(self):
return f'{self.student} enrolled in {self.course} ({self.semester_term} {self.semester_year})'
@property
def attendance_percentage(self):
if self.total_classes > 0:
return (self.classes_attended / self.total_classes) * 100
return 0
def is_passing(self):
"""Check if student is passing the course"""
passing_grades = ['A', 'B', 'C', 'D']
return self.grade in passing_grades if self.grade else None
# Self-referential many-to-many
class Person(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField(unique=True)
# Many-to-many relationship with itself
friends = models.ManyToManyField(
'self',
through='Friendship',
symmetrical=False, # Friendship might not be mutual
related_name='friend_of'
)
def __str__(self):
return self.name
class Friendship(models.Model):
"""Through model for Person-Person friendship"""
from_person = models.ForeignKey(
Person,
on_delete=models.CASCADE,
related_name='friendships_initiated'
)
to_person = models.ForeignKey(
Person,
on_delete=models.CASCADE,
related_name='friendships_received'
)
created_date = models.DateField(auto_now_add=True)
is_confirmed = models.BooleanField(default=False)
class Meta:
unique_together = [['from_person', 'to_person']]
def __str__(self):
status = 'confirmed' if self.is_confirmed else 'pending'
return f'{self.from_person} -> {self.to_person} ({status})'
Understanding Django relationships enables you to model complex real-world data structures efficiently while maintaining data integrity and providing intuitive APIs for accessing related data.
Defining Fields
Django model fields are the building blocks of your data structure. Each field type corresponds to a specific database column type and provides validation, conversion, and rendering capabilities. Understanding field options and customization is crucial for creating robust data models.
Examples of Relationship Patterns
Real-world applications require sophisticated relationship patterns to model complex business logic. This section provides comprehensive examples of common relationship patterns you'll encounter in Django applications.