Django 从入门到精通:知行博客实战(6.1) · 第 9 / 36 节

设计分类、文章与关系

吴秀峰 3 次阅读 0 条评论 ↓

学习目标

把博客业务转换为 Category、Post 两个模型,理解外键、删除策略、状态枚举、约束和索引,区分数据库字段和 Python 方法。

为什么先画关系再写字段

一篇文章有一个作者和一个分类;一个作者可以写多篇文章,一个分类也可以包含多篇文章。这就是两个“多对一”外键。标题不是可靠的唯一标识,因为同一标题可能重复;详情 URL 使用数据库主键,标题编辑不会改变地址。

状态分为 draft 和 published。新文章默认是草稿,页面只展示明确公开的数据。字段 choices 帮表单显示中文选项,CheckConstraint 让数据库也拒绝非法状态;二者作用不同。索引用于辅助已发布列表按时间查询,并不是给每个字段都建立索引。

具体怎么做

  1. 替换 blog/models.py。
  2. Category.slug 保存短地址标识,例如 django;名称保存“Django”。初学阶段手动输入英文 slug,避免中文名称自动转写为空。
  3. 作者和分类使用 PROTECT:存在文章时不能直接删除它们,避免误删整个内容集合。
  4. created_at 记录首次创建时间,updated_at 随模型 save 更新。QuerySet.update 不会自动执行模型 save,因此以后批量修改时间要显式处理。
  5. 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