第4章:Django模型(Models)
4.2 Django 模型关系
关系类型概述
Django提供了三种主要的关系字段来表示数据库表之间的关系:
- OneToOneField:一对一关系
- ForeignKey:一对多关系(多对一)
- 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定义了反向关系的名称。
- 没有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.titleon_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模型关系是构建复杂数据结构的基础:
- ✅ OneToOneField:一对一关系,用于扩展模型
- ✅ ForeignKey:一对多关系,最常用的关系类型
- ✅ ManyToManyField:多对多关系,支持中间模型
- ✅ 关系选项:on_delete、related_name等重要选项
- ✅ 复杂查询:跨关系查询和聚合统计
关键要点:
- 合理选择关系类型
- 正确设置on_delete行为
- 使用related_name提高代码可读性
- 利用中间模型存储额外信息
- 掌握跨关系查询技巧
下一篇
我们将学习模型方法和属性,以及Meta类的详细配置。