如何修改 .save() 上的 Django 模型字段,其值取决于传入的更改?

How to modify a Django Model field on `.save()` whose value depends on the incoming changes?

我在多个相关模型中有字段,其值完全来自正在保存的模型中的其他字段和相关模型中的字段。我想自动维护它们的价值,使它们始终 current/valid,所以我写了一个基础 class,每个模型都从中继承。它覆盖 .save().delete().

它几乎可以正常工作,除非通过更改名为 InfusateTracer 的模型之间的 M:M 关系的 through 模型触发多个更新( through 模型命名为 InfusateTracer)。因此,例如,我有一个创建 2 InfusateTracer 模型记录的测试,它会触发对 Infusate:

的更新
glu_t = Tracer.objects.create(compound=glu)
c16_t = Tracer.objects.create(compound=c16)
io = Infusate.objects.create(short_name="ti")
InfusateTracer.objects.create(infusate=io, tracer=glu_t, concentration=1.0)
InfusateTracer.objects.create(infusate=io, tracer=c16_t, concentration=2.0)

print(f"Name: {infusate.name}")
Infusate.objects.get(name="ti{C16:0-[5,6-13C5,17O1];glucose-[2,3-13C5,4-17O1]}")

save() 覆盖如下所示:

    def save(self, *args, **kwargs):
        # Set the changed value triggering this update so that the derived value of the automatically updated field reflects the new values:
        super().save(*args, **kwargs)
        # Update the fields that change due to the above change (if any)
        self.update_decorated_fields()

        # Note, I cannot call save again because I get a duplicate exception, so `update_decorated_fields` uses `setattr`:
        # super().save(*args, **kwargs)

        # Percolate changes up to the parents (if any)
        self.call_parent_updaters()

自动维护的字段更新在这里执行。请注意,要更新的字段、生成它们值的函数以及父级的 link 都在 get_my_updaters() 返回的全局中维护,其值来自我编写的应用于更新函数的装饰器:

    def update_decorated_fields(self):
        for updater_dict in self.get_my_updaters():
            update_fun = getattr(self, updater_dict["function"])
            update_fld = updater_dict["update_field"]
            if update_fld is not None:
                current_val = None
                # ... brevity edit
                new_val = update_fun()
                setattr(self, update_fld, new_val)
                print(f"Auto-updated {self.__class__.__name__}.{update_fld} using {update_fun.__qualname__} from [{current_val}] to [{new_val}]")

并且在 post 顶部的测试代码示例中,其中创建了 InfusateTracer linking 记录,此方法对于未完全发生的更新至关重要:

    def call_parent_updaters(self):
        parents = []
        for updater_dict in self.get_my_updaters():
            update_fun = getattr(self, updater_dict["function"])
            parent_fld = updater_dict["parent_field"]
            # ... brevity edit
            if parent_inst is not None and parent_inst not in parents:
                parents.append(parent_inst)

        for parent_inst in parents:
            if isinstance(parent_inst, MaintainedModel):
                parent_inst.save()
            elif parent_inst.__class__.__name__ == "ManyRelatedManager":
                if parent_inst.count() > 0 and isinstance(
                    parent_inst.first(), MaintainedModel
                ):
                    for mm_parent_inst in parent_inst.all():
                        mm_parent_inst.save()

这里是相关的有序调试输出:

Auto-updated Infusate.name using Infusate._name from [ti] to [ti{glucose-[2,3-13C5,4-17O1]}]
Auto-updated Infusate.name using Infusate._name from [ti{glucose-[2,3-13C5,4-17O1]}] to [ti{C16:0-[5,6-13C5,17O1];glucose-[2,3-13C5,4-17O1]}]
Name: ti{glucose-[2,3-13C5,4-17O1]}
DataRepo.models.infusate.Infusate.DoesNotExist: Infusate matching query does not exist.

请注意,输出 Name: ti{glucose-[2,3-13C5,4-17O1]} 不完整(即使上面的调试输出 完整的:ti{C16:0-[5,6-13C5,17O1];葡萄糖-[2,3-13C5,4-17O1]} ).它包含创建第一个 through 记录时产生的信息:

InfusateTracer.objects.create(infusate=io, tracer=glu_t, concentration=1.0)

但随后的 through 记录创建者:

InfusateTracer.objects.create(infusate=io, tracer=c16_t, concentration=2.0)

...(虽然所有 Auto-updated 调试输出都是正确的 - 并且是我期望看到的),但不是 Infusate 记录的最终值 name 字段(应由从最后 Auto-updated 调试输出中显示的 7 个不同记录收集的值组成(1 个 Infusate 记录、2 个 Tracer 记录和 4 个 TracerLabel 记录) )...

这是因为异步执行还是因为我应该使用 setattr 以外的东西来保存更改?我已经测试了很多次,结果总是一样。

顺便说一句,我游说我们的团队甚至不要拥有这些自动维护的字段,因为它们可能会因数据库更改而变得无效,但实验室人员显然喜欢拥有它们,因为供应商就是这样命名的化合物,他们希望能够 copy/paste 在搜索等中使用它们)。

这里的问题是对如何应用更改、何时将更改用于构建新派生字段值以及何时应调用 super().save 方法的误解。

在这里,我正在创建一条记录:

io = Infusate.objects.create(short_name="ti")

与这 2 条记录相关(也正在创建):

glu_t = Tracer.objects.create(compound=glu)
c16_t = Tracer.objects.create(compound=c16)

然后,这些记录在直通模型中链接在一起:

InfusateTracer.objects.create(infusate=io, tracer=glu_t, concentration=1.0)
InfusateTracer.objects.create(infusate=io, tracer=c16_t, concentration=2.0)

认为(错误地)我必须调用 super().save() 以便当字段值聚集在一起组成 name字段,这些传入的更改将包含在名称中。

但是,self 对象用于检索这些值。没保存也没关系

在这一点上,将问题中提供的代码中的一些空白包括在内是很有用的。这是 Infusate 模型的一部分:

class Infusate(MaintainedModel):

    id = models.AutoField(primary_key=True)
    name = models.CharField(...)
    short_name = models.CharField(...)
    tracers = models.ManyToManyField(
        Tracer,
        through="InfusateTracer",
    )

    @field_updater_function(generation=0, update_field_name="name")
    def _name(self):
        if self.tracers is None or self.tracers.count() == 0:
            return self.short_name
        return (
            self.short_name
            + "{"
            + ";".join(sorted(map(lambda o: o._name(), self.tracers.all())))
            + "}"
        )

这是我推断(错误地)表示在我可以访问值之前必须保存记录的错误:

ValueError: "<Infusate: >" needs to have a value for field "id" before this many-to-many relationship can be used.

当我尝试以下版本的 save 覆盖时:

    def save(self, *args, **kwargs):
        self.update_decorated_fields()
        super().save(*args, **kwargs)
        self.call_parent_updaters()

但这真正意味着我必须测试除 self.tracers is None 之外的其他内容,以查看是否存在任何 M:M 链接。我们可以简单地检查 self.id。如果是None,我们可以推断self.tracers不存在。所以这个问题的答案只是将 save 方法覆盖编辑为:

    def save(self, *args, **kwargs):
        self.update_decorated_fields()
        super().save(*args, **kwargs)
        self.call_parent_updaters()

并编辑为字段更新生成值的方法:

    @field_updater_function(generation=0, update_field_name="name")
    def _name(self):
        if self.id is None or self.tracers is None or self.tracers.count() == 0:
            return self.short_name
        return (
            self.short_name
            + "{"
            + ";".join(sorted(map(lambda o: o._name(), self.tracers.all())))
            + "}"
        )