Как изменить поле 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())))
+ "}"
)