Chapter 4: Django Models
4.2 Django Model Relationships
Relationship Types Overview
Django provides three main relationship fields to represent relationships between database tables:
- OneToOneField: One-to-one relationship
- ForeignKey: One-to-many relationship (many-to-one)
- ManyToManyField: Many-to-many relationship
Understanding these relationships is crucial for designing good data models.
One-to-One Relationship (OneToOneField)
A one-to-one relationship represents a unique correspondence between two models, often used to extend existing models.
Basic Usage
# blog/models.py
from django.db import models
from django.contrib.auth.models import User
class Profile(models.Model):
"""User profile extension"""
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name='profile',
verbose_name="User"
)
bio = models.TextField(max_length=500, blank=True, verbose_name="Biography")
location = models.CharField(max_length=100, blank=True, verbose_name="Location")
birth_date = models.DateField(null=True, blank=True, verbose_name="Birth Date")
avatar = models.ImageField(
upload_to='avatars/',
blank=True,
verbose_name="Avatar"
)
website = models.URLField(blank=True, verbose_name="Personal Website")
github_username = models.CharField(max_length=50, blank=True, verbose_name="GitHub Username")
# Social media fields
twitter_handle = models.CharField(max_length=50, blank=True, verbose_name="Twitter")
linkedin_profile = models.URLField(blank=True, verbose_name="LinkedIn")
# Settings fields
email_notifications = models.BooleanField(default=True, verbose_name="Email Notifications")
show_email_publicly = models.BooleanField(default=False, verbose_name="Show Email Publicly")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created Time")
updated_at = models.DateTimeField(auto_now=True, verbose_name="Updated Time")
class Meta:
verbose_name = "User Profile"
verbose_name_plural = "User Profiles"
def __str__(self):
return f"{self.user.username}'s Profile"
@property
def full_name(self):
"""Get full name"""
return self.user.get_full_name() or self.user.username
@property
def age(self):
"""Calculate age"""
if self.birth_date:
from datetime import date
today = date.today()
return today.year - self.birth_date.year - (
(today.month, today.day) < (self.birth_date.month, self.birth_date.day)
)
return None
# Automatic Profile creation signal
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):
"""Automatically create a Profile when a User is created"""
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
"""Save Profile when User is saved"""
if hasattr(instance, 'profile'):
instance.profile.save()Using One-to-One Relationships
# Create user and profile
user = User.objects.create_user(
username='john_doe',
email='john@example.com',
first_name='John',
last_name='Doe'
)
# Profile will be automatically created through signals
profile = user.profile
profile.bio = "I am a Python developer"
profile.location = "Beijing"
profile.website = "https://johndoe.com"
profile.save()
# Reverse query
profile = Profile.objects.get(id=1)
user = profile.user
# Access through related_name
user = User.objects.get(username='john_doe')
bio = user.profile.bio # Using related_name='profile'related_name
related_name defines the name of the reverse relationship.
- Without related_name
# Assuming Enrollment model has a foreign key to Course
course = models.ForeignKey(Course, on_delete=models.CASCADE)
# Usage:
course = Course.objects.get(id=1)
enrollments = course.enrollment_set.all() # Default reverse relationship name- With related_name
# Assuming Enrollment model has a foreign key to Course
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='enrollments')
# Usage:
course = Course.objects.get(id=1)
enrollments = course.enrollments.all() # Using related_nameAdvanced One-to-One Relationship Example
class ArticleStatistics(models.Model):
"""Article statistics"""
article = models.OneToOneField(
'Article',
on_delete=models.CASCADE,
related_name='statistics',
verbose_name="Article"
)
# View statistics
total_views = models.PositiveIntegerField(default=0, verbose_name="Total Views")
unique_views = models.PositiveIntegerField(default=0, verbose_name="Unique Views")
today_views = models.PositiveIntegerField(default=0, verbose_name="Today's Views")
# Interaction statistics
total_likes = models.PositiveIntegerField(default=0, verbose_name="Total Likes")
total_shares = models.PositiveIntegerField(default=0, verbose_name="Total Shares")
total_comments = models.PositiveIntegerField(default=0, verbose_name="Total Comments")
# Time statistics
average_read_time = models.DurationField(null=True, blank=True, verbose_name="Average Read Time")
last_viewed_at = models.DateTimeField(null=True, blank=True, verbose_name="Last Viewed Time")
# Source statistics
search_engine_views = models.PositiveIntegerField(default=0, verbose_name="Search Engine Views")
social_media_views = models.PositiveIntegerField(default=0, verbose_name="Social Media Views")
direct_views = models.PositiveIntegerField(default=0, verbose_name="Direct Views")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Article Statistics"
verbose_name_plural = "Article Statistics"
def __str__(self):
return f"Statistics for {self.article.title}"One-to-Many Relationship (ForeignKey)
One-to-many relationships are the most common database relationships, representing that one instance of a model can be associated with multiple instances of another model.
Basic Usage
class Category(models.Model):
"""Article category"""
name = models.CharField(max_length=100, verbose_name="Category Name")
slug = models.SlugField(unique=True, verbose_name="URL Alias")
description = models.TextField(blank=True, verbose_name="Description")
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='children',
verbose_name="Parent Category"
)
class Meta:
verbose_name = "Category"
verbose_name_plural = "Categories"
def __str__(self):
return self.name
class Article(models.Model):
"""Article model"""
title = models.CharField(max_length=200, verbose_name="Title")
content = models.TextField(verbose_name="Content")
# Foreign key relationships
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='articles',
verbose_name="Author"
)
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='articles',
verbose_name="Category"
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Article"
verbose_name_plural = "Articles"
def __str__(self):
return self.titleon_delete Options Explained
The on_delete parameter defines the behavior when the referenced object is deleted:
class RelationshipExamples(models.Model):
# CASCADE: Cascade delete, delete this object when the referenced object is deleted
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='authored_articles'
)
# SET_NULL: Set to NULL when the referenced object is deleted
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
blank=True
)
# SET_DEFAULT: Set to default value
status = models.ForeignKey(
'Status',
on_delete=models.SET_DEFAULT,
default=1
)
# PROTECT: Protect, prevent deletion of the referenced object
important_category = models.ForeignKey(
Category,
on_delete=models.PROTECT,
related_name='protected_articles'
)
# SET(): Set to specified value
def get_default_user():
return User.objects.get_or_create(username='deleted_user')[0]
editor = models.ForeignKey(
User,
on_delete=models.SET(get_default_user),
related_name='edited_articles'
)
# DO_NOTHING: Do nothing (may cause database integrity errors)
reviewer = models.ForeignKey(
User,
on_delete=models.DO_NOTHING,
related_name='reviewed_articles'
)Self-Referencing Relationships
class Comment(models.Model):
"""Comment model, supports replying to comments"""
content = models.TextField(verbose_name="Comment Content")
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Author")
article = models.ForeignKey(Article, on_delete=models.CASCADE, verbose_name="Article")
# Self-reference: reply to comment
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='replies',
verbose_name="Parent Comment"
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Comment"
verbose_name_plural = "Comments"
ordering = ['created_at']
def __str__(self):
return f"{self.author.username}: {self.content[:50]}..."
@property
def is_reply(self):
"""Check if it's a reply comment"""
return self.parent is not None
def get_replies(self):
"""Get all replies"""
return self.replies.all()
def get_thread(self):
"""Get the entire comment thread"""
if self.parent:
return self.parent.get_thread()
return selfOne-to-Many Relationship Queries
# Forward query (from the many side to the one side)
article = Article.objects.get(id=1)
author = article.author
category = article.category
# Reverse query (from the one side to the many side)
user = User.objects.get(username='john')
user_articles = user.articles.all() # Using related_name
category = Category.objects.get(slug='tech')
category_articles = category.articles.all()
# Filter queries
published_articles = user.articles.filter(status='published')
recent_articles = category.articles.filter(created_at__gte=timezone.now() - timedelta(days=7))
# Count queries
article_count = user.articles.count()
published_count = user.articles.filter(status='published').count()Many-to-Many Relationship (ManyToManyField)
Many-to-many relationships represent a many-to-many correspondence between two models.
Basic Usage
class Tag(models.Model):
"""Tag model"""
name = models.CharField(max_length=50, unique=True, verbose_name="Tag Name")
slug = models.SlugField(unique=True, verbose_name="URL Alias")
description = models.TextField(blank=True, verbose_name="Description")
color = models.CharField(max_length=7, default='#007bff', verbose_name="Color")
class Meta:
verbose_name = "Tag"
verbose_name_plural = "Tags"
ordering = ['name']
def __str__(self):
return self.name
class Article(models.Model):
"""Article model"""
title = models.CharField(max_length=200, verbose_name="Title")
content = models.TextField(verbose_name="Content")
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Author")
# Many-to-many relationship
tags = models.ManyToManyField(
Tag,
blank=True,
related_name='articles',
verbose_name="Tags"
)
# Favorite feature: users can favorite multiple articles, articles can be favorited by multiple users
favorited_by = models.ManyToManyField(
User,
blank=True,
related_name='favorite_articles',
verbose_name="Favorite Users"
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Article"
verbose_name_plural = "Articles"
def __str__(self):
return self.titleIntermediate Model (Through Model)
When you need to store extra information in a many-to-many relationship, you can use an intermediate model:
class ArticleTag(models.Model):
"""Article-Tag intermediate model"""
article = models.ForeignKey(Article, on_delete=models.CASCADE)
tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
added_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Added By")
added_at = models.DateTimeField(auto_now_add=True, verbose_name="Added Time")
weight = models.PositiveIntegerField(default=1, verbose_name="Weight")
class Meta:
unique_together = ['article', 'tag']
verbose_name = "Article Tag Relationship"
verbose_name_plural = "Article Tag Relationships"
def __str__(self):
return f"{self.article.title} - {self.tag.name}"
class Article(models.Model):
# ... other fields ...
# Many-to-many relationship using intermediate model
tags = models.ManyToManyField(
Tag,
through='ArticleTag',
blank=True,
related_name='articles',
verbose_name="Tags"
)More Complex Many-to-Many Relationship Example
class Course(models.Model):
"""Course model"""
name = models.CharField(max_length=200, verbose_name="Course Name")
description = models.TextField(verbose_name="Course Description")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Course"
verbose_name_plural = "Courses"
def __str__(self):
return self.name
class Enrollment(models.Model):
"""Student enrollment intermediate model"""
GRADE_CHOICES = [
('A', 'A'),
('B', 'B'),
('C', 'C'),
('D', 'D'),
('F', 'F'),
]
student = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='enrollments',
verbose_name="Student"
)
course = models.ForeignKey(
Course,
on_delete=models.CASCADE,
related_name='enrollments',
verbose_name="Course"
)
# Additional fields
enrolled_at = models.DateTimeField(auto_now_add=True, verbose_name="Enrollment Time")
grade = models.CharField(max_length=1, choices=GRADE_CHOICES, blank=True, verbose_name="Grade")
is_completed = models.BooleanField(default=False, verbose_name="Is Completed")
completion_date = models.DateField(null=True, blank=True, verbose_name="Completion Date")
class Meta:
unique_together = ['student', 'course']
verbose_name = "Enrollment Record"
verbose_name_plural = "Enrollment Records"
def __str__(self):
return f"{self.student.username} - {self.course.name}"
class Student(models.Model):
"""Student model"""
user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name="User")
student_id = models.CharField(max_length=20, unique=True, verbose_name="Student ID")
# Many-to-many relationship through intermediate model
courses = models.ManyToManyField(
Course,
through='Enrollment',
related_name='students',
verbose_name="Courses"
)
class Meta:
verbose_name = "Student"
verbose_name_plural = "Students"
def __str__(self):
return f"{self.user.username} ({self.student_id})"Many-to-Many Relationship Operations
# Create objects
article = Article.objects.create(title="Django Tutorial", content="...")
tag1 = Tag.objects.create(name="Django", slug="django")
tag2 = Tag.objects.create(name="Python", slug="python")
# Add many-to-many relationships
article.tags.add(tag1)
article.tags.add(tag2)
# Or add in batch
article.tags.add(tag1, tag2)
# Or use IDs
article.tags.add(1, 2)
# Remove relationships
article.tags.remove(tag1)
article.tags.remove(tag1, tag2)
# Clear all relationships
article.tags.clear()
# Set relationships (will replace existing relationships)
article.tags.set([tag1, tag2])
article.tags.set([1, 2]) # Using IDs
# Query
article_tags = article.tags.all()
tag_articles = tag1.articles.all()
# Check if relationship exists
if tag1 in article.tags.all():
print("Article contains this tag")
# Operations when using intermediate model
enrollment = Enrollment.objects.create(
student=student,
course=course,
grade='A'
)
# Query intermediate model
enrollments = Enrollment.objects.filter(student=student)
student_courses = student.courses.all()Blog Article and Tag Example
Let's create a complete blog system to demonstrate all types of relationships:
# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.urls import reverse
class Category(models.Model):
"""Category model - demonstrates self-referencing ForeignKey"""
name = models.CharField(max_length=100, verbose_name="Category Name")
slug = models.SlugField(unique=True, verbose_name="URL Alias")
description = models.TextField(blank=True, verbose_name="Description")
# Self-referencing foreign key, supports multi-level categories
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='children',
verbose_name="Parent Category"
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Category"
verbose_name_plural = "Categories"
def __str__(self):
if self.parent:
return f"{self.parent.name} > {self.name}"
return self.name
def get_absolute_url(self):
return reverse('blog:category_detail', kwargs={'slug': self.slug})
def get_full_path(self):
"""Get full category path"""
path = [self.name]
parent = self.parent
while parent:
path.insert(0, parent.name)
parent = parent.parent
return ' > '.join(path)
class Tag(models.Model):
"""Tag model"""
name = models.CharField(max_length=50, unique=True, verbose_name="Tag Name")
slug = models.SlugField(unique=True, verbose_name="URL Alias")
color = models.CharField(max_length=7, default='#007bff', verbose_name="Color")
class Meta:
verbose_name = "Tag"
verbose_name_plural = "Tags"
def __str__(self):
return self.name
class Article(models.Model):
"""Article model - demonstrates various relationships"""
STATUS_CHOICES = [
('draft', 'Draft'),
('published', 'Published'),
('archived', 'Archived'),
]
title = models.CharField(max_length=200, verbose_name="Title")
slug = models.SlugField(unique_for_date='publish_date', verbose_name="URL Alias")
content = models.TextField(verbose_name="Content")
excerpt = models.TextField(max_length=300, blank=True, verbose_name="Excerpt")
# ForeignKey relationships
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='articles',
verbose_name="Author"
)
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='articles',
verbose_name="Category"
)
# ManyToMany relationships
tags = models.ManyToManyField(
Tag,
blank=True,
related_name='articles',
verbose_name="Tags"
)
# Favorite feature - many-to-many relationship between users and articles
favorited_by = models.ManyToManyField(
User,
blank=True,
related_name='favorite_articles',
through='Favorite',
verbose_name="Favorite Users"
)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
publish_date = models.DateTimeField(default=timezone.now)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Article"
verbose_name_plural = "Articles"
ordering = ['-publish_date']
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('blog:article_detail', kwargs={'slug': self.slug})
class Favorite(models.Model):
"""Favorite intermediate model"""
user = models.ForeignKey(User, on_delete=models.CASCADE)
article = models.ForeignKey(Article, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Favorite Time")
notes = models.TextField(blank=True, verbose_name="Favorite Notes")
class Meta:
unique_together = ['user', 'article']
verbose_name = "Favorite"
verbose_name_plural = "Favorites"
def __str__(self):
return f"{self.user.username} favorited {self.article.title}"
class UserProfile(models.Model):
"""User profile - demonstrates OneToOne relationship"""
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name='profile',
verbose_name="User"
)
bio = models.TextField(max_length=500, blank=True, verbose_name="Biography")
avatar = models.ImageField(upload_to='avatars/', blank=True, verbose_name="Avatar")
website = models.URLField(blank=True, verbose_name="Personal Website")
location = models.CharField(max_length=100, blank=True, verbose_name="Location")
birth_date = models.DateField(null=True, blank=True, verbose_name="Birth Date")
# Social media
github = models.CharField(max_length=50, blank=True, verbose_name="GitHub")
twitter = models.CharField(max_length=50, blank=True, verbose_name="Twitter")
# Following relationship - many-to-many relationship between users
following = models.ManyToManyField(
User,
blank=True,
related_name='followers',
symmetrical=False,
verbose_name="Following Users"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "User Profile"
verbose_name_plural = "User Profiles"
def __str__(self):
return f"{self.user.username}'s Profile"
# Signal handler, automatically create UserProfile
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=User)
def create_or_update_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
else:
instance.profile.save()Using Relationships for Complex Queries
# Query examples
from django.db.models import Count, Q
from blog.models import Article, Category, Tag, User
# 1. Query all published articles by a specific user
user = User.objects.get(username='john')
published_articles = user.articles.filter(status='published')
# 2. Query the number of articles in a specific category
tech_category = Category.objects.get(slug='tech')
article_count = tech_category.articles.filter(status='published').count()
# 3. Query articles containing a specific tag
django_articles = Article.objects.filter(tags__slug='django', status='published')
# 4. Query the most favorited articles
popular_articles = Article.objects.annotate(
favorite_count=Count('favorited_by')
).order_by('-favorite_count')
# 5. Query articles published by users that a specific user is following
user = User.objects.get(username='john')
following_articles = Article.objects.filter(
author__in=user.profile.following.all(),
status='published'
).order_by('-publish_date')
# 6. Complex query: Query articles containing multiple tags
articles_with_multiple_tags = Article.objects.filter(
tags__slug__in=['django', 'python']
).annotate(tag_count=Count('tags')).filter(tag_count__gte=2)
# 7. Query all articles in a category and its subcategories
def get_category_articles(category):
# Get category and all its subcategories
categories = Category.objects.filter(
Q(id=category.id) | Q(parent=category)
)
return Article.objects.filter(
category__in=categories,
status='published'
)Summary
Django model relationships are the foundation for building complex data structures:
- ✅ OneToOneField: One-to-one relationship, used for extending models
- ✅ ForeignKey: One-to-many relationship, the most commonly used relationship type
- ✅ ManyToManyField: Many-to-many relationship, supports intermediate models
- ✅ Relationship Options: Important options like on_delete, related_name
- ✅ Complex Queries: Cross-relationship queries and aggregation statistics
Key Points:
- Choose relationship types appropriately
- Set on_delete behavior correctly
- Use related_name to improve code readability
- Utilize intermediate models to store extra information
- Master cross-relationship query techniques
Next
We will learn about model methods and properties, and detailed configuration of the Meta class.
4.3 Django Model Properties and Methods →