Skip to content

Chapter 4: Django Models

4.2 Django Model Relationships

Relationship Types Overview

Django provides three main relationship fields to represent relationships between database tables:

  1. OneToOneField: One-to-one relationship
  2. ForeignKey: One-to-many relationship (many-to-one)
  3. 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

python
# 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

python
# 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 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_name

Advanced One-to-One Relationship Example

python
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

python
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.title

on_delete Options Explained

The on_delete parameter defines the behavior when the referenced object is deleted:

python
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

python
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 self

One-to-Many Relationship Queries

python
# 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

python
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.title

Intermediate Model (Through Model)

When you need to store extra information in a many-to-many relationship, you can use an intermediate model:

python
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

python
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

python
# 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:

python
# 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

python
# 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:

  1. OneToOneField: One-to-one relationship, used for extending models
  2. ForeignKey: One-to-many relationship, the most commonly used relationship type
  3. ManyToManyField: Many-to-many relationship, supports intermediate models
  4. Relationship Options: Important options like on_delete, related_name
  5. 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 →

Contents

Back to Course Outline

Released under the [BY-NC-ND License](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.en).