Как изменить поле Django Model на `.save()`, значение которого зависит от входящих изменений?

У меня есть поля в нескольких связанных моделях, значения которых полностью производны от других полей как в сохраняемой модели, так и от полей в связанных моделях. Я хотел автоматизировать поддержание их значений, чтобы они всегда были актуальными/действительными, поэтому я написал базовый класс, от которого наследуется каждая модель. Он переопределяет .save() и .delete().

В основном все работает, за исключением случаев, когда несколько обновлений запускаются через изменения в модели through отношения M:M. Например, у меня есть тест, который создает 2 записи модели through, что вызывает обновление модели с именем 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:
        # super().save(*args, **kwargs)

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

Здесь происходит автоматическое обновление полей. Обратите внимание, что поля для обновления, функция, генерирующая их значение, и ссылка на родителя хранятся в глобале, возвращаемом 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
                # Get the field to make sure it exists in the model
                try:
                    current_val = getattr(self, update_fld)
                except AttributeError as ae:
                    raise BadModelField(self.__class__.__name__, update_fld, update_fun.__qualname__)
                new_val = update_fun()
                setattr(self, update_fld, new_val)
                if current_val is None or current_val == "":
                    current_val = "<empty>"
                print(f"Auto-updated {self.__class__.__name__}.{update_fld} using {update_fun.__qualname__} from [{current_val}] to [{new_val}]")

В примере тестового кода в верхней части этого сообщения, где создаются InfusateTracer связывающие записи, этот метод имеет решающее значение для обновлений, которые происходят не полностью:

    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"]
            if parent_fld is not None:
                print(f"Looking in {self.__class__.__name__} for {parent_fld}")
                try:
                    parent_inst = getattr(self, parent_fld)
                except AttributeError as ae:
                    raise BadModelField(self.__class__.__name__, parent_fld, update_fun.__qualname__)
                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):
                print(f"Calling the linked {parent_inst.__class__.__name__} instance's save method.")
                parent_inst.save()
            elif parent_inst.__class__.__name__ == "ManyRelatedManager":
                if parent_inst.count() > 0 and isinstance(
                    parent_inst.first(), MaintainedModel
                ):
                    print(f"Calling every M:M linked {parent_inst.first().__class__.__name__} instance's save method.")
                    for mm_parent_inst in parent_inst.all():
                        print(f"Calling every M:M linked {mm_parent_inst.__class__.__name__} instance's save method.")
                        mm_parent_inst.save()
                elif parent_inst.count() > 0:
                    raise NotMaintained(parent_inst.first(), self)
                # Nothing to to do if there are no linked records
            else:
                raise NotMaintained(parent_inst, self)

А вот соответствующий упорядоченный отладочный вывод:

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.

Обратите внимание, что вывод Имя: ti{глюкоза-[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 корректны - и это то, что я ожидал увидеть, это не окончательное значение поля name записи Infusate (которое должно состоять из значений, собранных из 7 различных записей, как показано в последнем отладочном выводе Auto-updated (1 запись Infusate, 2 записи Tracer и 4 записи TracerLabel))...

Это происходит из-за асинхронного выполнения или потому, что я должен использовать что-то другое, кроме setattr для сохранения изменений? Я тестировал это много раз, и результат всегда один и тот же.

По случайному совпадению, я лоббировал в нашей команде отказ от автоматического ведения этих полей из-за их потенциальной недействительности при изменениях в БД, но сотрудникам лаборатории нравится, что они есть, потому что именно так поставщики называют соединения, и они хотят иметь возможность копировать/вставлять их в поиск и т.д.).

Проблема здесь заключается в неправильном понимании того, как применяются изменения, когда они используются при построении нового значения производного поля и когда следует вызывать метод 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())))
            + "}"
        )
Вернуться на верх