模型、约束与安全迁移
# tickets/models.py
from django.conf import settings
from django.db import models
from django.db.models import Q
class Ticket(models.Model):
class Status(models.TextChoices):
OPEN = "open", "Open"
CLAIMED = "claimed", "Claimed"
CLOSED = "closed", "Closed"
title = models.CharField(max_length=200)
status = models.CharField(max_length=16, choices=Status, default=Status.OPEN)
assignee = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="assigned_tickets",
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
indexes = [models.Index(fields=["status", "-created_at"])]
constraints = [
models.CheckConstraint(
condition=Q(status="open", assignee__isnull=True)
| Q(status="claimed", assignee__isnull=False)
| Q(status="closed"),
name="ticket_status_assignee_consistent",
)
]
def __str__(self):
return self.title用数据库替换第二站的字典:
from django.shortcuts import get_object_or_404, render
from .models import Ticket
def ticket_list(request):
return render(request, "tickets/list.html", {"tickets": Ticket.objects.all()})
def ticket_detail(request, pk):
return render(request, "tickets/detail.html", {"ticket": get_object_or_404(Ticket, pk=pk)})迁移纪律
先新增可空字段,再分批回填,再加非空约束,最后清理兼容代码。大表的默认值、索引和约束可能持锁;生成迁移不等于安全发布迁移。
自定义用户模型应在项目第一次迁移前决定。中途替换 AUTH_USER_MODEL 涉及外键、历史迁移和第三方应用,不是普通字段重命名。
5.2 / 6.0 边界
5.2 引入复合主键,但关联字段和 Admin 等能力仍需逐项核对限制。工单示例继续使用默认单列主键,避免为了展示新 API 把生态兼容风险带入主线。
验收
模型检查与迁移检查均通过;生成 SQL 已审查;非法状态/负责人组合被数据库约束拒绝;数据迁移可反向执行。
练习(先独立完成)
- 为标题增加禁止纯空白的应用校验,并说明数据库还缺什么保证。
- 增加 priority 约束和索引,查看 SQL。
- 写一个可逆
RunPython数据迁移。
深度加餐:约束是数据库里的最后一位同事
模型校验服务于用户体验,数据库约束服务于所有写入者;两者不能互相冒充。
先做一个可观察实验
from django.db import models
from django.db.models import Q
class Coupon(models.Model):
code = models.CharField(max_length=40)
active = models.BooleanField(default=True)
used_by = models.ForeignKey('auth.User', null=True, blank=True, on_delete=models.PROTECT)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['code'],
condition=Q(active=True),
name='uniq_active_coupon_code',
),
]预期:同码只有一张激活券;并发写入时以数据库约束为准,应用层的 clean 只是提前给出友好提示。
原理下潜与工程边界
migration 是 schema 的版本控制;AddField、RenameField、RunPython 的可逆性和锁表时间都要评估。先加可空字段、回填、再收紧约束,通常比一次性 NOT NULL 更安全。
Model.clean 不会被 QuerySet.update 自动调用;数据迁移应使用 apps.get_model,避免绑定未来模型;大表回填要分批并观察锁、redo 和复制延迟。
反例排错路线
每次迁移先 sqlmigrate 看 SQL,再在影子库演练;生产失败时保留迁移编号、数据库错误和已执行步骤,不要手工改 django_migrations 表糊弄记录。
练习加码(仍不提供答案)
加码:为已存在的订单增加不可为空的 currency 字段,设计三步迁移并写回滚说明。
验收时请补充:正常路径、权限或失败路径、以及一条能证明事务/资源/配置边界没有被绕过的测试。
继续查阅:Django 官方文档(外部资料已收录于证据账本) · Django 安全清单(外部资料已收录于证据账本)
课程合同:模型、约束与迁移
学习目标
学完本篇,读者应能把「重复工单被数据库唯一约束拒绝,迁移计划可审查」拆成输入、可信边界、Django 机制和可复查证据;同时能说明 Django 6.0 与 5.2 LTS 的差异不会改变业务不变量。
心智模型
请求/数据输入 → 边界检查 → Django 核心机制 → 可观察输出 → 恢复动作。配置、数据库提交和外部副作用分别拥有自己的生命周期;没有证据的“应该可以”不算完成。
本地机制图
图中的“输入”是不可信的 HTTP、表单、任务或配置;“边界”负责身份、格式、权限、大小、超时与版本;“机制”才是 URL、View、ORM、事务、缓存、Tasks 或 ASGI;“证据”是测试、响应、日志、查询计划和发布记录。
最小实验
在全新虚拟环境中运行:
python -m django --version
python manage.py check
python manage.py test实验只验证一个机制:输入一条有效值,再输入一条空值、重复值或无权限值。每次实验都记录 Django 版本、数据库、命令和退出码。
完整场景
把实验接到 tickets 工单流:请求携带用户身份和工单 ID,经过当前专题负责的边界,完成一次读或写,并留下响应状态、数据库变化、日志字段和清理动作。完整场景必须能第二次执行,或明确说明幂等键、事务回滚、缓存失效和任务重试策略。
运行输出
成功路径应出现:
Django 6.0.x
System check identified no issues (0 silenced).
OK失败路径要保留真实信号,例如 400(输入无效)、403(权限/CSRF)、404(资源不存在)、405(方法不允许)或数据库约束异常;不要把失败改写成含糊的 200。
失败边界
至少验证:空输入、重复提交、匿名用户、越权对象、并发写入、下游不可用、超时和版本不兼容。记录“谁拒绝、何时拒绝、状态是否回滚、重试是否安全”。开发模式的 traceback 不得进入生产响应,缓存不得跨用户复用敏感结果。
原理与排错
先确认版本和 settings,再用 manage.py check 定位启动问题;接着确认 URL/任务入口、输入解析和权限;最后检查 SQL、事务提交、响应和副作用。若结果异常,用最小复现打开 SQL/请求 ID/任务 ID 观测,沿「复现 → 缩小 → 观测 → 修复 → 回归」闭环,不靠重装依赖猜测。
练习(正文不泄答案)
- 为上述完整场景补一条正常路径和两条失败路径测试,并记录完整输出。
- 画出本篇机制图,标出身份 owner、数据提交点、缓存/任务副作用和恢复动作。
- 故意制造一次重复、越权或超时,说明应返回的状态、日志字段和是否可重试。
- 写一条 Django 5.2 LTS 兼容验收标准,再说明 Django 6.0 的新能力是否需要条件分支。
验收标准:命令可重复、输出可复查、正文没有练习答案;完成后再打开本阶段答案册。
答案册
本阶段答案册提供参考实现、关键推理、失败边界和替代方案;先独立完成练习再对照。
下一篇
下一篇是03-models-and-migrations/02-data-migrations-user-model,继续沿同一条输入—边界—机制—证据链前进。