2.1 模型字段与 Meta 设计 本节摘要:模型类是数据库表在 Python 世界的对应物,字段是列,属性是约束。本节以墨迹博客的文章模型为例,讲字段类型的选择逻辑、高频参数的真实含义、自定义管理器的价值,以及 Meta 类如何表达"表级规则"。选错字段类型是最贵的建模错误,因为它要到数据堆积之后才发作。 从业务事实出发 设计模型前先回答问题,而不是先想字段。墨迹博客要管理"文章",关于文章我们知道什么事实? 每篇有标题、正文、发布状态、创建时间 标题必填且不宜过长;正文可能很长;状态只有几种取值 创建时间由系统记录,不该手工填 把这些事实逐条翻译成字段,模型就基本成形: 字段类型的选择逻辑 Django 提供二三十种字段类型,常用的就那几类。
本节摘要:模型类是数据库表在 Python 世界的对应物,字段是列,属性是约束。本节以墨迹博客的文章模型为例,讲字段类型的选择逻辑、高频参数的真实含义、自定义管理器的价值,以及 Meta 类如何表达"表级规则"。选错字段类型是最贵的建模错误,因为它要到数据堆积之后才发作。
设计模型前先回答问题,而不是先想字段。墨迹博客要管理"文章",关于文章我们知道什么事实?
把这些事实逐条翻译成字段,模型就基本成形:
from django.db import models from django.contrib.auth.models import User class Article(models.Model): DRAFT, PUBLISHED = 0, 1 STATUS_CHOICES = [ (DRAFT, "草稿"), (PUBLISHED, "已发布"), ] title = models.CharField("标题", max_length=200) body = models.TextField("正文") status = models.SmallIntegerField( "状态", choices=STATUS_CHOICES, default=DRAFT ) author = models.ForeignKey( User, on_delete=models.CASCADE, related_name="articles", verbose_name="作者", ) created_at = models.DateTimeField("创建时间", auto_now_add=True) updated_at = models.DateTimeField("更新时间", auto_now=True) class Meta: ordering = ["-created_at"] verbose_name = "文章" verbose_name_plural = verbose_name indexes = [models.Index(fields=["status", "-created_at"])] def __str__(self): return self.title
Django 提供二三十种字段类型,常用的就那几类。选择依据不是"名字像不像",而是三个问题:数据有没有长度上限?要不要参与数值运算?取值是不是有限集合?
| 业务事实 | 应选字段 | 常见误用 | 误用的代价 |
|---|---|---|---|
| 有上限的短文本 | CharField 加 max_length | TextField 一把梭 | 丧失数据库层约束,脏数据长驱直入 |
| 大段正文 | TextField | CharField | 部分数据库直接报错或截断 |
| 有限取值 | choices 配 SmallIntegerField | 自由 CharField | "已发布/发布/yifabu"满天飞,统计失真 |
| 金额 | DecimalField 定精度 | FloatField | 浮点误差,对账时两分钱查一晚上 |
| 时间戳 | DateTimeField | 存字符串 | 无法按时间排序过滤,时区灾难 |
| 布尔 | BooleanField | SmallIntegerField 存 0/1 | 多一层心算,查询条件易写错 |
两个"自动时间"参数最容易混:auto_now_add 在首次创建时记录一次,适合"创建时间";auto_now 在每次保存时刷新,适合"更新时间"。想要用户可手工指定的时间,两个都别用,加 default 并允许为空。
💡 一个被低估的习惯:给每个字段写第一个位置参数(人类可读名)。Admin 后台、表单标签、第 9 章测试的报错信息都会用它,一次书写处处受益。
字段表达"列"的规则,Meta 表达"表"的规则。上面模型用到了四处:
class Meta: constraints = [ models.UniqueConstraint( fields=["author", "title"], name="unique_author_title" ), ]
模型上除了字段还能挂方法和"管理器"。管理器是模型通往数据库的默认入口——你每天写的 objects 就是它。默认管理器只有基础能力,为高频查询定制管理器能让语义聚拢在模型层:
class PublishedManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(status=Article.PUBLISHED) class Article(models.Model): # ... 字段同上 ... objects = models.Manager() published = PublishedManager()
此后 Article.published.all() 天然只含已发布文章,视图与模板无须再关心状态过滤——这正是第 1 章说的"过滤逻辑属于模型层"的落地。第 8 章做 Admin、第 9 章写测试时,这个入口会被反复复用。
__str__ 方法也值得一题:它决定模型实例在 Admin 下拉框、日志、调试输出里的显示名。不写的话,你看到的将是一串"Article object (3)",排查问题相当难受。
最后说说空值与缺省的取舍,这是字段设计里被问得最多的细节。null 参数控制数据库层面可否为空,blank 控制表单校验可否留空,两者相互独立——最常见的组合误区是对字符串字段随手写上 null=True,而在关系型数据库的约定里,字符列的"没有值"应该用空字符串表达,null 留给日期、外键这类确实需要"尚未确定"语义的字段。墨迹博客的实践是:标题正文一律不许 null,发布时间允许 null 表达"尚未发布"。这类约定单个看无关痛痒,成千上万行数据之后再统一,就是一次伤筋动骨的迁移——又是那句话,建模期的一切决定都伴随项目终身。
单个模型的骨架立起来了,下一节处理模型与模型之间的关系——外键、多对多,以及删数据时的连锁反应。