Django外键反向查询机制解析

Django中不使用 _set 语法,是因为Django在未显式定义 related_name 时,为多个指向同一模型的外键自动生成了唯一的反向名称。

🔍 原理分析:为什么你的代码可以工作

在你的 ChengWuKaoQinBiao 模型中,有两个外键指向了 JiaoLuBiao

class ChengWuKaoQinBiao(models.Model):
    """
    职工乘务交路考勤表
    """

    date_start = models.DateField(verbose_name="本月结算始发日期", default=timezone.now)
    date_end = models.DateField(verbose_name="本月结算终到日期", default=timezone.now)
    employee_jiaolu_team = models.ForeignKey(
        Employee, on_delete=models.CASCADE, verbose_name="乘务员"
    )
    jiaolu_team = models.ForeignKey(
        Team, on_delete=models.CASCADE, verbose_name="乘务时所在班组"
    )
    jiaolu_zhiwu = models.ForeignKey(
        ZhiWu, on_delete=models.CASCADE, verbose_name="乘务时职务"
    )
    # train_jiaolu = models.JSONField(verbose_name='乘务车次', default=list)
    train_jiaolu = models.ForeignKey(
        JiaoLuBiao, on_delete=models.CASCADE, verbose_name="乘务车次"
    )
    ti_cheng_employee = models.ForeignKey(
        Employee,
        on_delete=models.CASCADE,
        verbose_name="替乘人员",
        blank=True,
        null=True,
        related_name="ti_cheng_model",
    )
    calculate_cheng_wu_fei = models.BooleanField(
        default=True,
        verbose_name="计算乘务费",
        help_text="勾选表示此乘务记录计入乘务费计算"
    )
    calculate_shi_ji_gua_gou = models.BooleanField(
        default=True,
        verbose_name="计算实际挂钩工资",
        help_text="勾选表示此乘务记录计入实际挂钩工资计算"
    )
    ji_xiao_train = models.ForeignKey(
        JiaoLuBiao,
        on_delete=models.CASCADE,
        verbose_name="绩效核算车次",
        blank=True,
        null=True,
        related_name="ji_xiao_train_model",
    )
    ji_xiao_xi_shu = models.DecimalField(
        max_digits=5, decimal_places=2, verbose_name="绩效系数", default=1.0
    )
    custom_jiao_lu_type = models.ForeignKey(
        JiaoLuType,
        on_delete=models.CASCADE,
        verbose_name="自定义交路类型",
        blank=True,
        null=True,  # 允许为空,保存时会自动设置
        help_text="默认使用乘务车次的交路类型",
    )
    custom_xi_shu = models.BooleanField(default=False, verbose_name="是否手动修改系数")

    def save(self, *args, **kwargs):
        if not self.custom_xi_shu:
            self.ji_xiao_xi_shu = self.jiaolu_zhiwu.ji_xiao_xi_shu

            # 设置默认的交路类型(如果未设置且train_jiaolu存在)
            if not self.custom_jiao_lu_type and self.train_jiaolu_id:
                # 获取train_jiaolu的第一个关联交路类型
                first_type = self.train_jiaolu.jiao_lu_type.first()
                if first_type:
                    self.custom_jiao_lu_type = first_type
        super().save(*args, **kwargs)

    # 在保存之前可以进行数据验证或格式化
    # if not isinstance(self.train_jiaolu, dict):
    #     raise ValueError("train_jiaolu必须是字典列表")
    # super().save(*args, **kwargs)

    def __str__(self):
        return (
            self.employee_jiaolu_team.name
            + ":"
            + self.date_start.strftime("%Y-%m-%d")
            + " 至 "
            + self.date_end.strftime("%Y-%m-%d")
            + " "
            + self.train_jiaolu.train
            + "次"
        )


    class Meta:
        unique_together = (
            ("employee_jiaolu_team", "date_start", "date_end", "train_jiaolu"),
        )

        verbose_name = "职工乘务交路考勤表"
        verbose_name_plural = verbose_name


train_jiaolu = models.ForeignKey(
    JiaoLuBiao, on_delete=models.CASCADE, verbose_name="乘务车次"
)
ji_xiao_train = models.ForeignKey(
    JiaoLuBiao,
    on_delete=models.CASCADE,
    verbose_name="绩效核算车次",
    blank=True,
    null=True,
    related_name="ji_xiao_train_model",  # 这个你显式设置了
)

这里的关键点在于:

  1. Django 的默认行为:通常,如果只有一个外键指向某个模型且未设置 related_name,Django 会使用 小写模型名_set 作为默认的反向查询名(例如 jiaolubiao_set)。

  2. 多外键时的自动处理:当同一个模型(这里是 ChengWuKaoQinBiao)有多个外键指向另一个模型(JiaoLuBiao)时,Django 要求这些反向关系名称必须是唯一的。如果你没有为所有外键显式设置 related_name,Django 就无法使用统一的 小写模型名_set,因为它无法区分你具体想查询哪个外键关系。因此,Django 会自动为你生成一个唯一的反向名称。你代码中使用的 chengwukaoqinbiao 正是Django为 train_jiaolu 这个外键字段自动生成的默认反向名称。

💡 最佳实践与建议

虽然当前代码可以运行,但为了代码的清晰性和可维护性,我强烈建议你:

  • 显式设置 related_name:为所有外键关系手动指定一个清晰、明确的反向名称,避免依赖Django的自动生成规则。这能让你的代码意图更明确,也便于后续维护。

    例如,你可以将 train_jiaolu 字段修改为:

  • train_jiaolu = models.ForeignKey(
        JiaoLuBiao, 
        on_delete=models.CASCADE, 
        verbose_name="乘务车次",
        related_name="chengwu_kaoqin_records"  # 添加一个清晰的名称
    )
    之后你的查询就可以使用 chengwu_kaoqin_records 了。
  •  检查实际生成的反向名称:你可以通过查看项目的迁移文件,或者使用Django shell进行检查,来确认Django最终为你的模型生成了哪些具体的反向关系名称。

 

总而言之,你观察到的现象是Django在处理模型多外键关系时的一种长期存在的机制,并非5.2版本的新改动。养成显式定义 related_name 的习惯,会让你的项目结构更清晰。

如果你能分享这两个模型(JiaoLuBiao 和 ChengWuKaoQinBiao)完整的定义,或许我能提供更精确的反向关系名称解析。

 

posted @ 2025-11-02 09:28  笑而不语心自闲  阅读(5)  评论(0)    收藏  举报