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.
# models.py
from django.db import models
from django.contrib.auth.models import User
from django.core.validators import MinValueValidator, MaxValueValidator
class Category(models.Model):
"""Hierarchical product categories"""
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='children'
)
description = models.TextField(blank=True)
image = models.ImageField(upload_to='categories/', blank=True)
is_active = models.BooleanField(default=True)
sort_order = models.PositiveIntegerField(default=0)
class Meta:
verbose_name_plural = 'Categories'
ordering = ['sort_order', 'name']
def __str__(self):
return self.name
def get_full_path(self):
"""Get full category path like 'Electronics > Phones > Smartphones'"""
path = [self.name]
parent = self.parent
while parent:
path.insert(0, parent.name)
parent = parent.parent
return ' > '.join(path)
def get_all_children(self):
"""Get all descendant categories"""
children = list(self.children.all())
for child in self.children.all():
children.extend(child.get_all_children())
return children
class Brand(models.Model):
"""Product brands"""
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True)
logo = models.ImageField(upload_to='brands/', blank=True)
description = models.TextField(blank=True)
website = models.URLField(blank=True)
def __str__(self):
return self.name
class Product(models.Model):
"""Base product model"""
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
description = models.TextField()
short_description = models.CharField(max_length=500, blank=True)
# Relationships
category = models.ForeignKey(
Category,
on_delete=models.CASCADE,
related_name='products'
)
brand = models.ForeignKey(
Brand,
on_delete=models.CASCADE,
related_name='products'
)
# Basic info
sku = models.CharField(max_length=50, unique=True)
is_active = models.BooleanField(default=True)
is_featured = models.BooleanField(default=False)
# SEO
meta_title = models.CharField(max_length=60, blank=True)
meta_description = models.CharField(max_length=160, blank=True)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['category', 'is_active']),
models.Index(fields=['brand', 'is_active']),
models.Index(fields=['is_featured', 'is_active']),
]
def __str__(self):
return self.name
def get_price_range(self):
"""Get min and max prices from variants"""
variants = self.variants.filter(is_active=True)
if variants:
prices = variants.values_list('price', flat=True)
return min(prices), max(prices)
return None, None
def get_main_image(self):
"""Get primary product image"""
main_image = self.images.filter(is_primary=True).first()
return main_image.image if main_image else None
class ProductVariant(models.Model):
"""Product variants (size, color, etc.)"""
product = models.ForeignKey(
Product,
on_delete=models.CASCADE,
related_name='variants'
)
name = models.CharField(max_length=100) # e.g., "Red - Large"
sku = models.CharField(max_length=50, unique=True)
# Pricing
price = models.DecimalField(max_digits=10, decimal_places=2)
compare_at_price = models.DecimalField(
max_digits=10,
decimal_places=2,
null=True,
blank=True,
help_text="Original price for discount display"
)
# Inventory
stock_quantity = models.PositiveIntegerField(default=0)
low_stock_threshold = models.PositiveIntegerField(default=5)
# Physical attributes
weight = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)
dimensions = models.CharField(max_length=100, blank=True) # "10x5x2 cm"
# Status
is_active = models.BooleanField(default=True)
is_default = models.BooleanField(default=False)
class Meta:
ordering = ['name']
def __str__(self):
return f"{self.product.name} - {self.name}"
@property
def is_in_stock(self):
return self.stock_quantity > 0
@property
def is_low_stock(self):
return self.stock_quantity <= self.low_stock_threshold
@property
def discount_percentage(self):
if self.compare_at_price and self.compare_at_price > self.price:
return ((self.compare_at_price - self.price) / self.compare_at_price) * 100
return 0
class AttributeType(models.Model):
"""Types of product attributes (Color, Size, Material, etc.)"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
class AttributeValue(models.Model):
"""Values for attributes (Red, Large, Cotton, etc.)"""
attribute_type = models.ForeignKey(
AttributeType,
on_delete=models.CASCADE,
related_name='values'
)
value = models.CharField(max_length=100)
color_code = models.CharField(max_length=7, blank=True) # For color attributes
class Meta:
unique_together = [['attribute_type', 'value']]
def __str__(self):
return f"{self.attribute_type.name}: {self.value}"
class ProductVariantAttribute(models.Model):
"""Link variants to their attribute values"""
variant = models.ForeignKey(
ProductVariant,
on_delete=models.CASCADE,
related_name='attributes'
)
attribute_value = models.ForeignKey(
AttributeValue,
on_delete=models.CASCADE
)
class Meta:
unique_together = [['variant', 'attribute_value']]
class ProductImage(models.Model):
"""Product images"""
product = models.ForeignKey(
Product,
on_delete=models.CASCADE,
related_name='images'
)
variant = models.ForeignKey(
ProductVariant,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='images'
)
image = models.ImageField(upload_to='products/')
alt_text = models.CharField(max_length=200)
is_primary = models.BooleanField(default=False)
sort_order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ['sort_order']
# models.py (continued)
from decimal import Decimal
class Customer(models.Model):
"""Customer profile extending User"""
user = models.OneToOneField(User, on_delete=models.CASCADE)
phone = models.CharField(max_length=20, blank=True)
birth_date = models.DateField(null=True, blank=True)
# Customer metrics
total_orders = models.PositiveIntegerField(default=0)
total_spent = models.DecimalField(max_digits=12, decimal_places=2, default=0)
def __str__(self):
return self.user.get_full_name() or self.user.username
class Address(models.Model):
"""Customer addresses"""
ADDRESS_TYPES = [
('billing', 'Billing'),
('shipping', 'Shipping'),
]
customer = models.ForeignKey(
Customer,
on_delete=models.CASCADE,
related_name='addresses'
)
type = models.CharField(max_length=10, choices=ADDRESS_TYPES)
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
company = models.CharField(max_length=100, blank=True)
address_line_1 = models.CharField(max_length=200)
address_line_2 = models.CharField(max_length=200, blank=True)
city = models.CharField(max_length=100)
state = models.CharField(max_length=100)
postal_code = models.CharField(max_length=20)
country = models.CharField(max_length=100)
is_default = models.BooleanField(default=False)
class Meta:
verbose_name_plural = 'Addresses'
def __str__(self):
return f"{self.first_name} {self.last_name} - {self.city}"
class Cart(models.Model):
"""Shopping cart"""
customer = models.OneToOneField(
Customer,
on_delete=models.CASCADE,
related_name='cart'
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"Cart for {self.customer}"
def get_total_items(self):
return sum(item.quantity for item in self.items.all())
def get_subtotal(self):
return sum(item.get_total_price() for item in self.items.all())
class CartItem(models.Model):
"""Items in shopping cart"""
cart = models.ForeignKey(
Cart,
on_delete=models.CASCADE,
related_name='items'
)
variant = models.ForeignKey(
ProductVariant,
on_delete=models.CASCADE
)
quantity = models.PositiveIntegerField(default=1)
added_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = [['cart', 'variant']]
def __str__(self):
return f"{self.quantity}x {self.variant}"
def get_total_price(self):
return self.quantity * self.variant.price
class Order(models.Model):
"""Customer orders"""
STATUS_CHOICES = [
('pending', 'Pending'),
('processing', 'Processing'),
('shipped', 'Shipped'),
('delivered', 'Delivered'),
('cancelled', 'Cancelled'),
('refunded', 'Refunded'),
]
# Order identification
order_number = models.CharField(max_length=20, unique=True, editable=False)
customer = models.ForeignKey(
Customer,
on_delete=models.CASCADE,
related_name='orders'
)
# Status and dates
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
shipped_at = models.DateTimeField(null=True, blank=True)
delivered_at = models.DateTimeField(null=True, blank=True)
# Addresses (snapshot at time of order)
billing_address = models.TextField()
shipping_address = models.TextField()
# Pricing
subtotal = models.DecimalField(max_digits=10, decimal_places=2)
tax_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
shipping_cost = models.DecimalField(max_digits=10, decimal_places=2, default=0)
discount_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
# Notes
customer_notes = models.TextField(blank=True)
admin_notes = models.TextField(blank=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return f"Order {self.order_number}"
def save(self, *args, **kwargs):
if not self.order_number:
self.order_number = self.generate_order_number()
super().save(*args, **kwargs)
def generate_order_number(self):
import random
import string
while True:
number = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10))
if not Order.objects.filter(order_number=number).exists():
return number
@property
def total_amount(self):
return self.subtotal + self.tax_amount + self.shipping_cost - self.discount_amount
class OrderItem(models.Model):
"""Items in an order"""
order = models.ForeignKey(
Order,
on_delete=models.CASCADE,
related_name='items'
)
# Product info (snapshot at time of order)
product_name = models.CharField(max_length=200)
variant_name = models.CharField(max_length=100)
sku = models.CharField(max_length=50)
# Pricing (snapshot at time of order)
unit_price = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.PositiveIntegerField()
# Reference to current product (may be null if product deleted)
variant = models.ForeignKey(
ProductVariant,
on_delete=models.SET_NULL,
null=True,
blank=True
)
def __str__(self):
return f"{self.quantity}x {self.product_name} - {self.variant_name}"
@property
def total_price(self):
return self.quantity * self.unit_price
# models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
class Author(models.Model):
"""Author profile extending User"""
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(max_length=500, blank=True)
avatar = models.ImageField(upload_to='authors/', blank=True)
website = models.URLField(blank=True)
social_links = models.JSONField(default=dict, blank=True)
def __str__(self):
return self.user.get_full_name() or self.user.username
class Category(models.Model):
"""Blog categories with hierarchy"""
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True)
description = models.TextField(blank=True)
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='children'
)
color = models.CharField(max_length=7, default='#007bff')
class Meta:
verbose_name_plural = 'Categories'
def __str__(self):
return self.name
class Tag(models.Model):
"""Blog tags"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
class Series(models.Model):
"""Blog post series"""
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
description = models.TextField()
author = models.ForeignKey(Author, on_delete=models.CASCADE)
class Meta:
verbose_name_plural = 'Series'
def __str__(self):
return self.title
class Post(models.Model):
"""Blog posts with advanced features"""
STATUS_CHOICES = [
('draft', 'Draft'),
('review', 'Under Review'),
('published', 'Published'),
('archived', 'Archived'),
]
# Basic info
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
excerpt = models.TextField(max_length=300, blank=True)
content = models.TextField()
# Relationships
author = models.ForeignKey(
Author,
on_delete=models.CASCADE,
related_name='posts'
)
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
related_name='posts'
)
tags = models.ManyToManyField(Tag, blank=True, related_name='posts')
series = models.ForeignKey(
Series,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='posts'
)
# Co-authors
co_authors = models.ManyToManyField(
Author,
blank=True,
related_name='co_authored_posts'
)
# Status and publishing
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
is_featured = models.BooleanField(default=False)
allow_comments = models.BooleanField(default=True)
# Dates
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
published_at = models.DateTimeField(null=True, blank=True)
# SEO
meta_title = models.CharField(max_length=60, blank=True)
meta_description = models.CharField(max_length=160, blank=True)
# Analytics
view_count = models.PositiveIntegerField(default=0)
like_count = models.PositiveIntegerField(default=0)
class Meta:
ordering = ['-published_at', '-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
def save(self, *args, **kwargs):
if self.status == 'published' and not self.published_at:
self.published_at = timezone.now()
super().save(*args, **kwargs)
class Comment(models.Model):
"""Nested comments system"""
post = models.ForeignKey(
Post,
on_delete=models.CASCADE,
related_name='comments'
)
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='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}'
def get_replies(self):
return self.replies.filter(is_approved=True)
class PostLike(models.Model):
"""Post likes/reactions"""
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='likes')
user = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = [['post', 'user']]
class ReadingList(models.Model):
"""User reading lists"""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='reading_lists')
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
posts = models.ManyToManyField(Post, blank=True)
is_public = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.user.username}'s {self.name}"
# models.py
from django.db import models
from django.contrib.auth.models import User
from django.core.validators import MinValueValidator, MaxValueValidator
class Instructor(models.Model):
"""Course instructors"""
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField()
expertise = models.TextField()
years_experience = models.PositiveIntegerField()
rating = models.DecimalField(
max_digits=3,
decimal_places=2,
validators=[MinValueValidator(0), MaxValueValidator(5)],
null=True,
blank=True
)
def __str__(self):
return self.user.get_full_name()
class CourseCategory(models.Model):
"""Course categories"""
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True)
description = models.TextField()
icon = models.CharField(max_length=50, blank=True) # CSS icon class
class Meta:
verbose_name_plural = 'Course Categories'
def __str__(self):
return self.name
class Course(models.Model):
"""Online courses"""
DIFFICULTY_CHOICES = [
('beginner', 'Beginner'),
('intermediate', 'Intermediate'),
('advanced', 'Advanced'),
]
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
description = models.TextField()
short_description = models.CharField(max_length=300)
# Relationships
instructor = models.ForeignKey(
Instructor,
on_delete=models.CASCADE,
related_name='courses'
)
co_instructors = models.ManyToManyField(
Instructor,
blank=True,
related_name='co_instructed_courses'
)
category = models.ForeignKey(
CourseCategory,
on_delete=models.CASCADE,
related_name='courses'
)
prerequisites = models.ManyToManyField(
'self',
blank=True,
symmetrical=False,
related_name='unlocks'
)
# Course details
difficulty_level = models.CharField(max_length=20, choices=DIFFICULTY_CHOICES)
duration_hours = models.PositiveIntegerField()
price = models.DecimalField(max_digits=8, decimal_places=2)
# Status
is_published = models.BooleanField(default=False)
is_featured = models.BooleanField(default=False)
# Dates
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Analytics
enrollment_count = models.PositiveIntegerField(default=0)
rating = models.DecimalField(
max_digits=3,
decimal_places=2,
null=True,
blank=True
)
def __str__(self):
return self.title
class Module(models.Model):
"""Course modules/chapters"""
course = models.ForeignKey(
Course,
on_delete=models.CASCADE,
related_name='modules'
)
title = models.CharField(max_length=200)
description = models.TextField()
order = models.PositiveIntegerField()
class Meta:
ordering = ['order']
unique_together = [['course', 'order']]
def __str__(self):
return f"{self.course.title} - {self.title}"
class Lesson(models.Model):
"""Individual lessons within modules"""
LESSON_TYPES = [
('video', 'Video'),
('text', 'Text'),
('quiz', 'Quiz'),
('assignment', 'Assignment'),
]
module = models.ForeignKey(
Module,
on_delete=models.CASCADE,
related_name='lessons'
)
title = models.CharField(max_length=200)
lesson_type = models.CharField(max_length=20, choices=LESSON_TYPES)
content = models.TextField(blank=True)
video_url = models.URLField(blank=True)
duration_minutes = models.PositiveIntegerField(default=0)
order = models.PositiveIntegerField()
is_free = models.BooleanField(default=False)
class Meta:
ordering = ['order']
unique_together = [['module', 'order']]
def __str__(self):
return f"{self.module.title} - {self.title}"
class Student(models.Model):
"""Student profiles"""
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(blank=True)
learning_goals = models.TextField(blank=True)
def __str__(self):
return self.user.get_full_name() or self.user.username
class Enrollment(models.Model):
"""Student course enrollments"""
student = models.ForeignKey(
Student,
on_delete=models.CASCADE,
related_name='enrollments'
)
course = models.ForeignKey(
Course,
on_delete=models.CASCADE,
related_name='enrollments'
)
enrolled_at = models.DateTimeField(auto_now_add=True)
completed_at = models.DateTimeField(null=True, blank=True)
progress_percentage = models.PositiveIntegerField(default=0)
class Meta:
unique_together = [['student', 'course']]
def __str__(self):
return f"{self.student} enrolled in {self.course}"
class LessonProgress(models.Model):
"""Track student progress through lessons"""
enrollment = models.ForeignKey(
Enrollment,
on_delete=models.CASCADE,
related_name='lesson_progress'
)
lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE)
is_completed = models.BooleanField(default=False)
completed_at = models.DateTimeField(null=True, blank=True)
time_spent_minutes = models.PositiveIntegerField(default=0)
class Meta:
unique_together = [['enrollment', 'lesson']]
class Assignment(models.Model):
"""Course assignments"""
lesson = models.OneToOneField(
Lesson,
on_delete=models.CASCADE,
related_name='assignment'
)
instructions = models.TextField()
max_score = models.PositiveIntegerField(default=100)
due_date = models.DateTimeField(null=True, blank=True)
def __str__(self):
return f"Assignment: {self.lesson.title}"
class Submission(models.Model):
"""Student assignment submissions"""
assignment = models.ForeignKey(
Assignment,
on_delete=models.CASCADE,
related_name='submissions'
)
student = models.ForeignKey(
Student,
on_delete=models.CASCADE,
related_name='submissions'
)
content = models.TextField()
file_upload = models.FileField(upload_to='submissions/', blank=True)
submitted_at = models.DateTimeField(auto_now_add=True)
# Grading
score = models.PositiveIntegerField(null=True, blank=True)
feedback = models.TextField(blank=True)
graded_at = models.DateTimeField(null=True, blank=True)
graded_by = models.ForeignKey(
Instructor,
on_delete=models.SET_NULL,
null=True,
blank=True
)
class Meta:
unique_together = [['assignment', 'student']]
def __str__(self):
return f"{self.student} - {self.assignment}"
These relationship patterns demonstrate how to model complex real-world scenarios using Django's ORM. Each pattern shows proper use of foreign keys, many-to-many relationships, and through models to create maintainable and efficient database schemas.
Relationships and Foreign Keys
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.
Making Queries
Django's ORM provides a powerful and intuitive API for querying your database. Understanding how to construct efficient queries is essential for building performant applications.