brechin / django-computed-property

Computed Property Fields for Django

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Computed value not saved unless previously accessed

SvenEV opened this issue · comments

I'm using Django 2.2.6 and I found that ComputedFields are not updated in the database when saving models unless these fields have been accessed before saving.

It looks like ComputedFields use the pre_safe signal to compute and store the values in the model's __dict__ just before the model is persisted in the database. The problem is that this is too late. At the time pre_save is triggered, the ComputedField has already been classified as "deferred" in Model.save(), thus it is not included in update_fields and not part of the generated SQL statement.

However, if the ComputedField has been accessed before saving, the computed value is already stored in __dict__ when the deferred fields are determined. Thus, the field is included in update_fields and it works as expected.

A simple, but easy to forget workaround is to explicitly access all ComputedFields before saving a model instance:

_ = model_instance.my_computed_property_1
_ = model_instance.my_computed_property_2
_ = model_instance.my_computed_property_3
model_instance.save()

A potential fix is to use the post_init signal to trigger value computation as soon as a model instance is created:

class ComputedField(models.Field):
    # ...
    def contribute_to_class(self, cls, name, **kwargs):
        # ...
        post_init.connect(self.init_computed_field, sender=cls)

    def init_computed_field(self, sender, instance, **kwargs):
        # trigger ObjectProxy.__get__, causing the calculated value to be stored in instance.__dict__
        getattr(instance, self.get_attname())

I could do a PR but since I am relatively new to Django I am not sure if my fix has any unintended side effects.

Can you provide an example of code that shows this behavior? I don't have an exact test for this, but there is a test that does a .objects.create and then a get using the computed value. This shows it was stored in the DB, and seems like it would mirror the same behavior as not explicitly accessing the computed field on the object.

ref:

def test_search(self, db, model, vals):
created = model.objects.create(base_value=vals[0])
fetched = model.objects.get(computed=vals[1])
assert created == fetched
assert created.id == fetched.id