Skip to content

第4章:Django模型(Models)

4.2 Django 模型关系

关系类型概述

Django提供了三种主要的关系字段来表示数据库表之间的关系:

  1. OneToOneField:一对一关系
  2. ForeignKey:一对多关系(多对一)
  3. ManyToManyField:多对多关系

理解这些关系对于设计良好的数据模型至关重要。

一对一关系(OneToOneField)

一对一关系表示两个模型之间的唯一对应关系,通常用于扩展现有模型。

基本用法

python
# blog/models.py
from django.db import models
from django.contrib.auth.models import User

class Profile(models.Model):
    """用户资料扩展"""
    user = models.OneToOneField(
        User,
        on_delete=models.CASCADE,
        related_name='profile',
        verbose_name="用户"
    )
    bio = models.TextField(max_length=500, blank=True, verbose_name="个人简介")
    location = models.CharField(max_length=100, blank=True, verbose_name="位置")
    birth_date = models.DateField(null=True, blank=True, verbose_name="出生日期")
    avatar = models.ImageField(
        upload_to='avatars/',
        blank=True,
        verbose_name="头像"
    )
    website = models.URLField(blank=True, verbose_name="个人网站")
    github_username = models.CharField(max_length=50, blank=True, verbose_name="GitHub用户名")
    
    # 社交媒体字段
    twitter_handle = models.CharField(max_length=50, blank=True, verbose_name="Twitter")
    linkedin_profile = models.URLField(blank=True, verbose_name="LinkedIn")
    
    # 设置字段
    email_notifications = models.BooleanField(default=True, verbose_name="邮件通知")
    show_email_publicly = models.BooleanField(default=False, verbose_name="公开邮箱")
    
    created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
    updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
    
    class Meta:
        verbose_name = "用户资料"
        verbose_name_plural = "用户资料"
    
    def __str__(self):
        return f"{self.user.username} 的资料"
    
    @property
    def full_name(self):
        """获取完整姓名"""
        return self.user.get_full_name() or self.user.username
    
    @property
    def age(self):
        """计算年龄"""
        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

# 自动创建Profile的信号
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):
    """用户创建时自动创建对应的Profile"""
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    """用户保存时同时保存Profile"""
    if hasattr(instance, 'profile'):
        instance.profile.save()

一对一关系的使用

python
# 创建用户和资料
user = User.objects.create_user(
    username='john_doe',
    email='john@example.com',
    first_name='John',
    last_name='Doe'
)

# Profile会通过信号自动创建
profile = user.profile
profile.bio = "我是一名Python开发者"
profile.location = "北京"
profile.website = "https://johndoe.com"
profile.save()

# 反向查询
profile = Profile.objects.get(id=1)
user = profile.user

# 通过related_name访问
user = User.objects.get(username='john_doe')
bio = user.profile.bio  # 使用related_name='profile'

related_name定义了反向关系的名称。

  • 没有related_name时
# 假设Enrollment模型有外键指向Course
course = models.ForeignKey(Course, on_delete=models.CASCADE)

# 使用时:
course = Course.objects.get(id=1)
enrollments = course.enrollment_set.all()  # 默认反向关系名
  • 有related_name时
# 假设Enrollment模型有外键指向Course
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='enrollments')

# 使用时:
course = Course.objects.get(id=1)
enrollments = course.enrollments.all()  # 使用related_name

高级一对一关系示例

python
class ArticleStatistics(models.Model):
    """文章统计信息"""
    article = models.OneToOneField(
        'Article',
        on_delete=models.CASCADE,
        related_name='statistics',
        verbose_name="文章"
    )
    
    # 浏览统计
    total_views = models.PositiveIntegerField(default=0, verbose_name="总浏览量")
    unique_views = models.PositiveIntegerField(default=0, verbose_name="独立浏览量")
    today_views = models.PositiveIntegerField(default=0, verbose_name="今日浏览量")
    
    # 互动统计
    total_likes = models.PositiveIntegerField(default=0, verbose_name="总点赞数")
    total_shares = models.PositiveIntegerField(default=0, verbose_name="总分享数")
    total_comments = models.PositiveIntegerField(default=0, verbose_name="总评论数")
    
    # 时间统计
    average_read_time = models.DurationField(null=True, blank=True, verbose_name="平均阅读时间")
    last_viewed_at = models.DateTimeField(null=True, blank=True, verbose_name="最后浏览时间")
    
    # 来源统计
    search_engine_views = models.PositiveIntegerField(default=0, verbose_name="搜索引擎来源")
    social_media_views = models.PositiveIntegerField(default=0, verbose_name="社交媒体来源")
    direct_views = models.PositiveIntegerField(default=0, verbose_name="直接访问")
    
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        verbose_name = "文章统计"
        verbose_name_plural = "文章统计"
    
    def __str__(self):
        return f"{self.article.title} 的统计信息"

一对多关系(ForeignKey)

一对多关系是最常见的数据库关系,表示一个模型的实例可以关联到另一个模型的多个实例。

基本用法

python
class Category(models.Model):
    """文章分类"""
    name = models.CharField(max_length=100, verbose_name="分类名称")
    slug = models.SlugField(unique=True, verbose_name="URL别名")
    description = models.TextField(blank=True, verbose_name="描述")
    parent = models.ForeignKey(
        'self',
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name='children',
        verbose_name="父分类"
    )
    
    class Meta:
        verbose_name = "分类"
        verbose_name_plural = "分类"
    
    def __str__(self):
        return self.name

class Article(models.Model):
    """文章模型"""
    title = models.CharField(max_length=200, verbose_name="标题")
    content = models.TextField(verbose_name="内容")
    
    # 外键关系
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='articles',
        verbose_name="作者"
    )
    category = models.ForeignKey(
        Category,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='articles',
        verbose_name="分类"
    )
    
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        verbose_name = "文章"
        verbose_name_plural = "文章"
    
    def __str__(self):
        return self.title

on_delete选项详解

on_delete参数定义了当被引用的对象被删除时的行为:

python
class RelationshipExamples(models.Model):
    # CASCADE:级联删除,删除引用对象时同时删除此对象
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='authored_articles'
    )
    
    # SET_NULL:设为NULL,被引用对象删除时此字段设为NULL
    category = models.ForeignKey(
        Category,
        on_delete=models.SET_NULL,
        null=True,
        blank=True
    )
    
    # SET_DEFAULT:设为默认值
    status = models.ForeignKey(
        'Status',
        on_delete=models.SET_DEFAULT,
        default=1
    )
    
    # PROTECT:保护,阻止删除被引用的对象
    important_category = models.ForeignKey(
        Category,
        on_delete=models.PROTECT,
        related_name='protected_articles'
    )
    
    # SET():设为指定值
    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:什么都不做(可能导致数据库完整性错误)
    reviewer = models.ForeignKey(
        User,
        on_delete=models.DO_NOTHING,
        related_name='reviewed_articles'
    )

自引用关系

python
class Comment(models.Model):
    """评论模型,支持回复评论"""
    content = models.TextField(verbose_name="评论内容")
    author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="作者")
    article = models.ForeignKey(Article, on_delete=models.CASCADE, verbose_name="文章")
    
    # 自引用:回复评论
    parent = models.ForeignKey(
        'self',
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name='replies',
        verbose_name="父评论"
    )
    
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        verbose_name = "评论"
        verbose_name_plural = "评论"
        ordering = ['created_at']
    
    def __str__(self):
        return f"{self.author.username}: {self.content[:50]}..."
    
    @property
    def is_reply(self):
        """判断是否为回复评论"""
        return self.parent is not None
    
    def get_replies(self):
        """获取所有回复"""
        return self.replies.all()
    
    def get_thread(self):
        """获取整个评论线程"""
        if self.parent:
            return self.parent.get_thread()
        return self

一对多关系的查询

python
# 正向查询(从多的一方查一的一方)
article = Article.objects.get(id=1)
author = article.author
category = article.category

# 反向查询(从一的一方查多的一方)
user = User.objects.get(username='john')
user_articles = user.articles.all()  # 使用related_name

category = Category.objects.get(slug='tech')
category_articles = category.articles.all()

# 过滤查询
published_articles = user.articles.filter(status='published')
recent_articles = category.articles.filter(created_at__gte=timezone.now() - timedelta(days=7))

# 统计查询
article_count = user.articles.count()
published_count = user.articles.filter(status='published').count()

多对多关系(ManyToManyField)

多对多关系表示两个模型之间的多对多对应关系。

基本用法

python
class Tag(models.Model):
    """标签模型"""
    name = models.CharField(max_length=50, unique=True, verbose_name="标签名称")
    slug = models.SlugField(unique=True, verbose_name="URL别名")
    description = models.TextField(blank=True, verbose_name="描述")
    color = models.CharField(max_length=7, default='#007bff', verbose_name="颜色")
    
    class Meta:
        verbose_name = "标签"
        verbose_name_plural = "标签"
        ordering = ['name']
    
    def __str__(self):
        return self.name

class Article(models.Model):
    """文章模型"""
    title = models.CharField(max_length=200, verbose_name="标题")
    content = models.TextField(verbose_name="内容")
    author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="作者")
    
    # 多对多关系
    tags = models.ManyToManyField(
        Tag,
        blank=True,
        related_name='articles',
        verbose_name="标签"
    )
    
    # 收藏功能:用户可以收藏多篇文章,文章可以被多个用户收藏
    favorited_by = models.ManyToManyField(
        User,
        blank=True,
        related_name='favorite_articles',
        verbose_name="收藏用户"
    )
    
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        verbose_name = "文章"
        verbose_name_plural = "文章"
    
    def __str__(self):
        return self.title

中间模型(Through Model)

当需要在多对多关系中存储额外信息时,可以使用中间模型:

python
class ArticleTag(models.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_at = models.DateTimeField(auto_now_add=True, verbose_name="添加时间")
    weight = models.PositiveIntegerField(default=1, verbose_name="权重")
    
    class Meta:
        unique_together = ['article', 'tag']
        verbose_name = "文章标签关系"
        verbose_name_plural = "文章标签关系"
    
    def __str__(self):
        return f"{self.article.title} - {self.tag.name}"

class Article(models.Model):
    # ... 其他字段 ...
    
    # 使用中间模型的多对多关系
    tags = models.ManyToManyField(
        Tag,
        through='ArticleTag',
        blank=True,
        related_name='articles',
        verbose_name="标签"
    )

更复杂的多对多关系示例

python
class Course(models.Model):
    """课程模型"""
    name = models.CharField(max_length=200, verbose_name="课程名称")
    description = models.TextField(verbose_name="课程描述")
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        verbose_name = "课程"
        verbose_name_plural = "课程"
    
    def __str__(self):
        return self.name

class Enrollment(models.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="学生"
    )
    course = models.ForeignKey(
        Course,
        on_delete=models.CASCADE,
        related_name='enrollments',
        verbose_name="课程"
    )
    
    # 额外字段
    enrolled_at = models.DateTimeField(auto_now_add=True, verbose_name="选课时间")
    grade = models.CharField(max_length=1, choices=GRADE_CHOICES, blank=True, verbose_name="成绩")
    is_completed = models.BooleanField(default=False, verbose_name="是否完成")
    completion_date = models.DateField(null=True, blank=True, verbose_name="完成日期")
    
    class Meta:
        unique_together = ['student', 'course']
        verbose_name = "选课记录"
        verbose_name_plural = "选课记录"
    
    def __str__(self):
        return f"{self.student.username} - {self.course.name}"

class Student(models.Model):
    """学生模型"""
    user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name="用户")
    student_id = models.CharField(max_length=20, unique=True, verbose_name="学号")
    
    # 通过中间模型的多对多关系
    courses = models.ManyToManyField(
        Course,
        through='Enrollment',
        related_name='students',
        verbose_name="课程"
    )
    
    class Meta:
        verbose_name = "学生"
        verbose_name_plural = "学生"
    
    def __str__(self):
        return f"{self.user.username} ({self.student_id})"

多对多关系的操作

python
# 创建对象
article = Article.objects.create(title="Django教程", content="...")
tag1 = Tag.objects.create(name="Django", slug="django")
tag2 = Tag.objects.create(name="Python", slug="python")

# 添加多对多关系
article.tags.add(tag1)
article.tags.add(tag2)
# 或者批量添加
article.tags.add(tag1, tag2)
# 或者使用ID
article.tags.add(1, 2)

# 移除关系
article.tags.remove(tag1)
article.tags.remove(tag1, tag2)

# 清空所有关系
article.tags.clear()

# 设置关系(会替换现有关系)
article.tags.set([tag1, tag2])
article.tags.set([1, 2])  # 使用ID

# 查询
article_tags = article.tags.all()
tag_articles = tag1.articles.all()

# 检查关系是否存在
if tag1 in article.tags.all():
    print("文章包含此标签")

# 使用中间模型时的操作
enrollment = Enrollment.objects.create(
    student=student,
    course=course,
    grade='A'
)

# 查询中间模型
enrollments = Enrollment.objects.filter(student=student)
student_courses = student.courses.all()

博客文章和标签示例

让我们创建一个完整的博客系统,展示所有类型的关系:

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):
    """分类模型 - 演示自引用ForeignKey"""
    name = models.CharField(max_length=100, verbose_name="分类名称")
    slug = models.SlugField(unique=True, verbose_name="URL别名")
    description = models.TextField(blank=True, verbose_name="描述")
    
    # 自引用外键,支持多级分类
    parent = models.ForeignKey(
        'self',
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name='children',
        verbose_name="父分类"
    )
    
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        verbose_name = "分类"
        verbose_name_plural = "分类"
    
    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):
        """获取完整分类路径"""
        path = [self.name]
        parent = self.parent
        while parent:
            path.insert(0, parent.name)
            parent = parent.parent
        return ' > '.join(path)

class Tag(models.Model):
    """标签模型"""
    name = models.CharField(max_length=50, unique=True, verbose_name="标签名称")
    slug = models.SlugField(unique=True, verbose_name="URL别名")
    color = models.CharField(max_length=7, default='#007bff', verbose_name="颜色")
    
    class Meta:
        verbose_name = "标签"
        verbose_name_plural = "标签"
    
    def __str__(self):
        return self.name

class Article(models.Model):
    """文章模型 - 演示各种关系"""
    STATUS_CHOICES = [
        ('draft', '草稿'),
        ('published', '已发布'),
        ('archived', '已归档'),
    ]
    
    title = models.CharField(max_length=200, verbose_name="标题")
    slug = models.SlugField(unique_for_date='publish_date', verbose_name="URL别名")
    content = models.TextField(verbose_name="内容")
    excerpt = models.TextField(max_length=300, blank=True, verbose_name="摘要")
    
    # ForeignKey关系
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='articles',
        verbose_name="作者"
    )
    category = models.ForeignKey(
        Category,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='articles',
        verbose_name="分类"
    )
    
    # ManyToMany关系
    tags = models.ManyToManyField(
        Tag,
        blank=True,
        related_name='articles',
        verbose_name="标签"
    )
    
    # 收藏功能 - 用户和文章的多对多关系
    favorited_by = models.ManyToManyField(
        User,
        blank=True,
        related_name='favorite_articles',
        through='Favorite',
        verbose_name="收藏用户"
    )
    
    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 = "文章"
        verbose_name_plural = "文章"
        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):
    """收藏中间模型"""
    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="收藏时间")
    notes = models.TextField(blank=True, verbose_name="收藏笔记")
    
    class Meta:
        unique_together = ['user', 'article']
        verbose_name = "收藏"
        verbose_name_plural = "收藏"
    
    def __str__(self):
        return f"{self.user.username} 收藏了 {self.article.title}"

class UserProfile(models.Model):
    """用户资料 - 演示OneToOne关系"""
    user = models.OneToOneField(
        User,
        on_delete=models.CASCADE,
        related_name='profile',
        verbose_name="用户"
    )
    bio = models.TextField(max_length=500, blank=True, verbose_name="个人简介")
    avatar = models.ImageField(upload_to='avatars/', blank=True, verbose_name="头像")
    website = models.URLField(blank=True, verbose_name="个人网站")
    location = models.CharField(max_length=100, blank=True, verbose_name="位置")
    birth_date = models.DateField(null=True, blank=True, verbose_name="出生日期")
    
    # 社交媒体
    github = models.CharField(max_length=50, blank=True, verbose_name="GitHub")
    twitter = models.CharField(max_length=50, blank=True, verbose_name="Twitter")
    
    # 关注关系 - 用户之间的多对多关系
    following = models.ManyToManyField(
        User,
        blank=True,
        related_name='followers',
        symmetrical=False,
        verbose_name="关注的用户"
    )
    
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        verbose_name = "用户资料"
        verbose_name_plural = "用户资料"
    
    def __str__(self):
        return f"{self.user.username} 的资料"

# 信号处理器,自动创建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()

使用关系进行复杂查询

python
# 查询示例
from django.db.models import Count, Q
from blog.models import Article, Category, Tag, User

# 1. 查询某个用户的所有已发布文章
user = User.objects.get(username='john')
published_articles = user.articles.filter(status='published')

# 2. 查询某个分类下的文章数量
tech_category = Category.objects.get(slug='tech')
article_count = tech_category.articles.filter(status='published').count()

# 3. 查询包含特定标签的文章
django_articles = Article.objects.filter(tags__slug='django', status='published')

# 4. 查询被收藏最多的文章
popular_articles = Article.objects.annotate(
    favorite_count=Count('favorited_by')
).order_by('-favorite_count')

# 5. 查询某用户关注的人发布的文章
user = User.objects.get(username='john')
following_articles = Article.objects.filter(
    author__in=user.profile.following.all(),
    status='published'
).order_by('-publish_date')

# 6. 复杂查询:查询包含多个标签的文章
articles_with_multiple_tags = Article.objects.filter(
    tags__slug__in=['django', 'python']
).annotate(tag_count=Count('tags')).filter(tag_count__gte=2)

# 7. 查询某分类及其子分类的所有文章
def get_category_articles(category):
    # 获取分类及其所有子分类
    categories = Category.objects.filter(
        Q(id=category.id) | Q(parent=category)
    )
    return Article.objects.filter(
        category__in=categories,
        status='published'
    )

小结

Django模型关系是构建复杂数据结构的基础:

  1. OneToOneField:一对一关系,用于扩展模型
  2. ForeignKey:一对多关系,最常用的关系类型
  3. ManyToManyField:多对多关系,支持中间模型
  4. 关系选项:on_delete、related_name等重要选项
  5. 复杂查询:跨关系查询和聚合统计

关键要点:

  • 合理选择关系类型
  • 正确设置on_delete行为
  • 使用related_name提高代码可读性
  • 利用中间模型存储额外信息
  • 掌握跨关系查询技巧

下一篇

我们将学习模型方法和属性,以及Meta类的详细配置。

4.3 Django 模型属性与方法 →

目录

返回课程目录

Released under the Apache 2.0 License.