Django 模型设计:如何优雅处理带可选子类型的题目分类结构

本文介绍一种更合理、健壮的 django 模型设计方案,用于表示“必有类型、可选子类型的题目分类关系,涵盖外键建模优化、`__str__` 安全实现及数据一致性保障。

在 Django 应用中,当业务模型需要表达「层级分类」关系(如题目必须属于某一大类,但可进一步细分为可选的子类)时,直接为顶层类型和子类型分别建立独立外键,虽直观却易引发冗余与逻辑矛盾。你当前的设计中,Question 同时持有 type(必填)和 type_subtype(可空)两个外键,这隐含了数据不一致风险:例如,一个 QuestionSubType 实例所属的 QuestionType 与 Question.type 可能不匹配,破坏分类完整性。

更优解是以子类型为事实中心——即 Question 仅通过 type_subtype 关联到 QuestionSubType,而 QuestionSubType 自身通过外键关联到 QuestionType。这样既保证了类型归属的唯一性,又自然支持「无子类型」场景(通过允许 type_subtype 为空),同时消除了跨字段校验负担。

以下是重构后的推荐模型结构:

class QuestionType(models.Model):
    name = models.CharField(max_length=255, unique=True)  # 建议使用更语义化的字段名

    def __str__(self):
        return self.name

class QuestionSubType(models.Model):
    question_type = models.ForeignKey(
        QuestionType,
        on_delete=models.CASCADE,
        related_name='subtypes'
    )
    name = models.CharField(max_length=255)

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=['question_type', 'name'],
                name='unique_type_subname'
            )
        ]

    def __str__(self):
        return f"{self.question_type.name} → {self.name}"

class Question(QuestionAbstractModel):
    chapter = models.ForeignKey(
        Chapter,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        related_name='questions'
    )
    type_subtype = models.ForeignKey(
        QuestionSubType,
        on_delete=models.SET_NULL,  # 推荐使用 SET_NULL 而非 CASCADE,避免误删题目
        blank=True,
        null=True,
        related_name='questions'
    )
    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 "No Chapter"
        )
        subtype_part = str(self.type_subtype) if self.type_subtype else "No Subtype"
        return f"{chapter_part} — {subtype_part}"

关键改进说明:

  • 单一可信源:Question 不再维护独立的 type 字段,类型信息完全由 type_subtype.question_type 提供,避免数据二义性;
  • 健壮的 __str__:对 self.chapter、self.chapter.subject 和 self.type_subtype 均做存在性检查,防止 None 引发 AttributeError;
  • 更安全的级联行为:将 on_delete=models.SET_NULL 应用于 chapter 和 type_subtype,确保删除章节或子类型时题目仍可保留(需对应字段设为 null=True);
  • 增强约束与可读性:QuestionSubType 添加联合唯一约束,防止同一类型下重复子类名;字段命名统一为 name,语义更清晰;
  • 正向反向关系明确:通过 related_name 显式定义反向关系(如 question_type.subtypes.all()),提升查询可读性。

最后提醒:若业务中存在大量“仅有类型、无子类型”的题目,还可考虑为 QuestionSubType 添加一个全局占位实例(如 Uncategorized),让 type_subtype 始终非空,从而简化前端逻辑——但这属于权衡取舍,需结合实际查询频次与一致性要求决定。