本文介绍一种更合理、可维护性更强的 django 模型结构,用于表示「必有类型、子类型可选」的题目分类需求,涵盖外键关系优化、`__str__` 安全实现及语义清晰的字段命名。
在 Django 中为「题目(Question)」建模时,若业务逻辑明确要求每个题目必须归属一个类型(Type),而子类型(Subtype)仅为可选补充信息,则原始设计中同时保留 type(指向 QuestionType)和 type_subtype(指向 QuestionSubType)两个外键的做法,虽能工作,但存在冗余、语义不清与潜在一致性风险。
我们应将 QuestionSubType 视为实际分类单元,而 QuestionType 仅作为逻辑分组(如 “选择题”、“填空题”、“解答题”),不再在 Question 中单独保存 type 字段。这样既避免数据重复,又天然保证了「有子类型则必有类型」的一致性约束:
class QuestionType(models.Model):
name = models.CharField(max_length=255, unique=True) # 更语义化的字段名
def __str__(self):
return self.name
class QuestionSubType(models.Model):
type = models.ForeignKey(QuestionType, on_delete=models.PROTECT, related_name='subtypes')
name = models.CharField(max_l
ength=255)
class Meta:
constraints = [
models.UniqueConstraint(fields=['type', 'name'], name='unique_type_subname')
]
def __str__(self):
return f"{self.type.name} → {self.name}" # 清晰体现层级关系对应地,Question 模型精简为:
class Question(QuestionAbstractModel):
chapter = models.ForeignKey(
Chapter,
on_delete=models.SET_NULL,
blank=True,
null=True,
help_text="所属章节(可为空)"
)
subtype = models.ForeignKey(
QuestionSubType,
on_delete=models.SET_NULL, # 推荐 SET_NULL 而非 CASCADE,避免误删导致题目丢失
blank=True,
null=True,
related_name='questions',
help_text="题目子类型(可为空)"
)
solution_url = models.URLField(max_length=555, blank=True)
def __str__(self):
# 安全处理可能为 None 的字段
chapter_part = (
f"{self.chapter.subject.grade} {self.chapter.subject.name} {self.chapter.name}"
if self.chapter and self.chapter.subject
else "无章节"
)
subtype_part = str(self.subtype) if self.subtype else "无子类型(仅基础类型)"
return f"[{chapter_part}] {subtype_part}"该设计遵循「单一数据源原则」:子类型承载全部分类语义,类型仅作归类容器。它简化了模型关系、规避了跨字段一致性校验难题,并提升了查询与管理的清晰度——例如,获取某类型下的所有题目,只需 QuestionType.objects.get(...).subtypes.all().questions.all(),链式调用自然、高效且意图明确。