Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is there a way to update a computedfield's last change? #128

Open
simkimsia opened this issue Jun 16, 2023 · 4 comments
Open

Is there a way to update a computedfield's last change? #128

simkimsia opened this issue Jun 16, 2023 · 4 comments

Comments

@simkimsia
Copy link
Contributor

let say

class SomeModel(ComputedFieldsModel):
    fieldA = ...

    @computed(..., depends=[('self', ['fieldA'])])
    def comp(self):
        # do something with self.fieldA
        return ...

so i want a comp_last_changed which is a datetime

how do I do this?

@jerch
Copy link
Collaborator

jerch commented Jun 16, 2023

Not directly, but you can create a dependent datetime field, that updates from changes on comp, something like this (untested):

class SomeModel(ComputedFieldsModel):
    fieldA = ...

    @computed(..., depends=[('self', ['fieldA'])])
    def comp(self):
        # do something with self.fieldA
        return ...

    @computed(models.DateTimeField(), depends=[('self', ['comp'])])
    def comp_last_changed(self):
        return timezone.now()

Edit: I am currently not sure, if the evaluation will be skipped if comp did not change, but some other computed field on that model (so this needs def. testing).

Edit2: Nope this wont work in that case, as the mro evaluation does not stop on non-changes:

for comp_field in mro:
new_value = self._compute(elem, model, comp_field)
if new_value != getattr(elem, comp_field):
has_changed = True
setattr(elem, comp_field, new_value)

I think this could be patched in, so far strict updating was no issue, thus the code will eval the function if in doubt, which make the approach above not working correctly.

@simkimsia
Copy link
Contributor Author

simkimsia commented Jun 16, 2023

🤔

forgot to add that my comp is a boolean, so when it flips, i need to track the last_changed

or in the original comp method i can get the old value?

EDIT1:

i just saw I think this could be patched in, so far strict updating was no issue, thus the code will eval the function if in doubt, which make the approach above not working correctly.

hmm i guess there's no easy way to do this?

@jerch
Copy link
Collaborator

jerch commented Jun 21, 2023

Right, thats not as easy as it sounds - currently the update resolver give no guarantees not to update a field in question. It resorts to "if in doubt - update anyway", since keeping values in sync is more important than anything else.

There is still a custom solution possible here (I admit its not nice) - the only field during cf updates, that knows whether comp changed is the comp function itself, thus you can expand on that like this (untested):

class SomeModel(ComputedFieldsModel):
    fieldA = ...
    comp_last_changed = models.DateTimeField()

    @computed(..., depends=[('self', ['fieldA'])])
    def comp(self):
        # grab old value
        old_value = self.comp
        # do something with self.fieldA
        new_value = ...
        # update conditionally on value change
        if old_value != new_value:
            self.comp_last_changed = timezone.now()
            if self.pk:
                self.save(update_fields=['comp_last_changed'])
        return new_value

The self.pk condition with an additional save call is unlucky, and in fact creates a double save for the same instance. Still it should get the trick done. The additional save work could be avoided, if the cf function would expose its internal call state, whether it was called from normal save or the resolver, and act upon those, sadly thats not possible atm. Maybe thats a better way to approach this issue?

Edit:
I think something like this should be possible to avoid the double save:

class SomeModel(ComputedFieldsModel):
    fieldA = ...
    comp_last_changed = models.DateTimeField()

    @computed(..., depends=[('self', ['fieldA'])], may_update=['comp_last_changed'])
    def comp(self):
        # grab old value
        old_value = self.comp
        # do something with self.fieldA
        new_value = ...
        # update conditionally on value change
        if old_value != new_value:
            self.comp_last_changed = timezone.now()
        return new_value

Note the additional kwarg may_update on the decorator, with that I can extend the update_fields clause in the resolver to also include that field during bulk_update (its not even a big change).

@jerch
Copy link
Collaborator

jerch commented Jun 22, 2023

Thinking again about the issue the ideas above are not ideal for these reasons:

  • the double save approach will create havoc, if comp_last_changed is part of a depends entry of some other cf (it will turn update execution upside-down)
  • an explicit kwargs on the decorator is too limited in what it can be used for (although it would work for your particular use case)

Imho a better and more versatile approach is to get signals from the update resolver done, see #58. So far I did not push that PR forward, as mass updates as normally done by the resolver create some issues about the right signal abstraction, like what types of signals we gonna need and in which transaction context the signal handlers should run. And for strict post signals (emitted after the resolver finished all transactions) it creates a lot of state to be held in memory.

Putting aside these issues for a moment, with the right signals something like this should be possible:

@receiver(post_change_computedfields, sender=SomeModel, field='comp')
def my_handler(sender, field, queryset, **kwargs):
    # queryset now contains only instances, where comp changed
    # do whatever you like based on those changes
    ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants