设计分类、文章与关系
学习目标
把博客业务转换为 Category、Post 两个模型,理解外键、删除策略、状态枚举、约束和索引,区分数据库字段和 Python 方法。
为什么先画关系再写字段
一篇文章有一个作者和一个分类;一个作者可以写多篇文章,一个分类也可以包含多篇文章。这就是两个“多对一”外键。标题不是可靠的唯一标识,因为同一标题可能重复;详情 URL 使用数据库主键,标题编辑不会改变地址。
状态分为 draft 和 published。新文章默认是草稿,页面只展示明确公开的数据。字段 choices 帮表单显示中文选项,CheckConstraint 让数据库也拒绝非法状态;二者作用不同。索引用于辅助已发布列表按时间查询,并不是给每个字段都建立索引。
具体怎么做
- 替换
blog/models.py。 - Category.slug 保存短地址标识,例如
django;名称保存“Django”。初学阶段手动输入英文 slug,避免中文名称自动转写为空。 - 作者和分类使用 PROTECT:存在文章时不能直接删除它们,避免误删整个内容集合。
created_at记录首次创建时间,updated_at随模型 save 更新。QuerySet.update 不会自动执行模型 save,因此以后批量修改时间要显式处理。get_absolute_url()统一生成详情地址。对应路由将在第 13 章创建,当前先不调用这个方法。
本章新增或替换的完整文件
以下路径均相对于练习项目根目录。替换同名文件的全部内容;未列出的文件保持上一章版本。
blog/models.py
from django.conf import settings
from django.db import models
from django.urls import reverse
class Category(models.Model):
name = models.CharField("名称", max_length=40, unique=True)
slug = models.SlugField("地址标识", unique=True)
class Meta:
ordering = ["name", "pk"]
def __str__(self):
return self.name
class Post(models.Model):
class Status(models.TextChoices):
DRAFT = "draft", "草稿"
PUBLISHED = "published", "已发布"
title = models.CharField("标题", max_length=120)
body = models.TextField("正文")
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="posts")
category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name="posts")
status = models.CharField("状态", max_length=12, choices=Status, default=Status.DRAFT)
created_at = models.DateTimeField("创建时间", auto_now_add=True)
updated_at = models.DateTimeField("修改时间", auto_now=True)
class Meta:
ordering = ["-created_at", "-pk"]
indexes = [models.Index(fields=["status", "-created_at"], name="post_status_created_idx")]
constraints = [models.CheckConstraint(condition=models.Q(status__in=["draft", "published"]), name="post_valid_status")]
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse("blog:detail", args=[self.pk])
如何确认成功
python manage.py check
python manage.py shell -c "from blog.models import Post; print(Post.Status.choices); print(Post._meta.get_field('author').remote_field.model)"
预期状态包含“草稿”“已发布”,作者指向自定义用户模型。尚未执行迁移时不要调用 Post.objects.count(),数据库表还不存在。
练习:写下如果删除分类,CASCADE、SET_NULL、PROTECT 分别会怎样影响文章。考虑一个归档系统应该选择哪一种,并说明理由。
新版本适配说明
Django 6.1 新增数据库执行的 DB_CASCADE、DB_SET_NULL、DB_SET_DEFAULT。它们会改变删除发生的位置,例如 DB_CASCADE 不触发 Django 删除信号;MySQL/MariaDB 不支持 DB_SET_DEFAULT。主线保留明确的 PROTECT 和后续评论 CASCADE,避免为了使用新 API 改变业务含义。现有系统从 Python 级联改成数据库级联前,需要检查信号、审计、数据库能力和迁移 SQL。
常见问题
blank 和 null 有什么区别? blank 主要影响校验,null 决定数据库是否允许 NULL;字符串通常使用空字符串表达空值。为什么排序加 -pk? 多条记录创建时间可能相同,增加唯一字段让分页顺序稳定。保存模型会自动 full_clean 吗? 不会,表单会参与模型校验,直接 ORM 写入时应明确校验责任并保留数据库约束。
官方参考
登录后可保存阅读位置、标记章节完成,并在其他设备继续学习。
登录并保存学习进度 →联系站长
微信:VIPdjango。技术问题建议先在 社区讨论 中交流,方便更多人参与和查阅。
章节讨论 0