102

I have a django model, and I need to compare old and new values of field BEFORE saving.

I've tried the save() inheritance, and pre_save signal. It was triggered correctly, but I can't find the list of actually changed fields and can't compare old and new values. Is there a way? I need it for optimization of pre-save actions.

Thank you!

11
  • 1
    What about fetchind old value from DB in save method and than checking each field for equality? Commented Apr 29, 2014 at 9:47
  • What kind of optimization do you want? Commented Apr 29, 2014 at 9:59
  • @J0HN The da between changed during the fetch, compare, and save processes. Commented Apr 29, 2014 at 10:03
  • I think, it can and must slow down the performance? Commented Apr 29, 2014 at 10:05
  • 1
    Possible duplicate of Django: When saving, how can you check if a field has changed? Commented Dec 6, 2017 at 23:54

11 Answers 11

90

There is very simple django way for doing it.

"Memorise" the values in model init like this:

def __init__(self, *args, **kwargs): super(MyClass, self).__init__(*args, **kwargs) self.initial_parametername = self.parametername --- self.initial_parameternameX = self.parameternameX 

Real life example:

At class:

def __init__(self, *args, **kwargs): super(MyClass, self).__init__(*args, **kwargs) self.__important_fields = ['target_type', 'target_id', 'target_object', 'number', 'chain', 'expiration_date'] for field in self.__important_fields: setattr(self, '__original_%s' % field, getattr(self, field)) def has_changed(self): for field in self.__important_fields: orig = '__original_%s' % field if getattr(self, orig) != getattr(self, field): return True return False 

And then in modelform save method:

def save(self, force_insert=False, force_update=False, commit=True): # Prep the data obj = super(MyClassForm, self).save(commit=False) if obj.has_changed(): # If we're down with commitment, save this shit if commit: obj.save(force_insert=True) return obj 

Edit ( 06.05.2024 ): Please read through all the comments that have been added since I added this post 10 years ago. Things have changed, and though even I still use this approach myself and recommend using it, there are cases, where you need to tweak the approach or forgo using it. Again, read through the comments others have added.

Sign up to request clarification or add additional context in comments.

13 Comments

I prefer the Odif's way, because I need to trigger the actions for model without forms (after changes comes from api or from admin site)
when is __init__ called? will it work only for initial creation or subsequent updates as well?
Init is called every time model instance is created. If instance is updated serveral times over it's lifetime, then __init__ is only called in the beginning.
this will not cover if the model is saved in other places with save or bulk_create
Be careful with this approach. I find a lot of issues (including python crashing or recursion limits reached) when trying to do i.e. Model.objects.delete() if field I want to cache this is foreign key (even if you try to store self._old_<field>_id as integer
|
66

It is better to do this at ModelForm level.

There you get all the Data that you need for comparison in save method:

  1. self.data : Actual Data passed to the Form.
  2. self.cleaned_data : Data cleaned after validations, Contains Data eligible to be saved in the Model
  3. self.changed_data : List of Fields which have changed. This will be empty if nothing has changed

If you want to do this at Model level then you can follow the method specified in Odif's answer.

6 Comments

I agree with your answer, also self.instance can be of use in this issue.
@AlexeyKuleshevich agreed, but only before a form's _post_clean (is_valid->errors->full_clean->_post_clean), after which the instance will be updated to include the new values. accessing in form.clean_fieldname() and form.clean() seems ok provided it's their first call.
Well that works, but ONLY if you're saving with a form, which isn't always the case.
Yeah, True. If you are not using a Form then you can't do this. But using a Form is the ideal way.
self.changed_data is a new for me
|
46

Also you can use FieldTracker from django-model-utils for this:

  1. Just add tracker field to your model:

    tracker = FieldTracker() 
  2. Now in pre_save and post_save you can use:

    instance.tracker.previous('modelfield') # get the previous value instance.tracker.has_changed('modelfield') # just check if it is changed 

5 Comments

Yeah I just love how clean this is... Another line to requirements!
But this tracker field is a real column in the table? Or is just a fake field?
@toscanelli, it does not add a column to the table.
Just a reminder to make sure to makemigrations and migrate again otherwise there will be an attribute error like: 'tracker' not found.
This one is so tempting but someone report a performance issue here. And there is no update nor follow up from the team. So, checkout the source code of tracker.py. It looks like a lot of works and signaling. So, it comes to if it worth - or the use case was too limited that you only need to track one field or two.
44

Django's documentation contains an example showing exactly how to do this:

Django 1.8+ and above (Including Django 2.x and 3.x), there is a from_db classmethod, which can be used to customize model instance creation when loading from the database.

Note: There is NO additional database query if you use this method.

from django.db import Model class MyClass(models.Model): @classmethod def from_db(cls, db, field_names, values): instance = super().from_db(db, field_names, values) # save original values, when model is loaded from database, # in a separate attribute on the model instance._loaded_values = dict(zip(field_names, values)) return instance 

So now the original values are available in the _loaded_values attribute on the model. You can access this attribute inside your save method to check if some value is being updated.

class MyClass(models.Model): field_1 = models.CharField(max_length=1) @classmethod def from_db(cls, db, field_names, values): ... # use code from above def save(self, *args, **kwargs): # check if a new db row is being added # When this happens the `_loaded_values` attribute will not be available if not self._state.adding: # check if field_1 is being updated if self._loaded_values['field_1'] != self.field_1: # do something super().save(*args, **kwargs) 

3 Comments

This is pretty cool, but it won't give you the M2M relations. For example if you are trying to track changes to what groups a User is associated with, there doesn't appear to be any way to do it with this technique.
Note that two or more consecutive calls to .save() for a new instance would trigger the not self._state.adding condition even though no _loaded_values exist. Best to just check for the existence of _loaded_values.
I use this a lot, but like @shacker commented, it is not working with M2M. Does anyone know of a straight forward implementation that would work for M2M too?
7

In modern Django, there is a matter of great importance to add to the content of the answer accepted among the above answers. You can fall into an infinite recursion when you use defer or only QuerySet API.

__get__() method of django.db.models.query_utils.DeferredAttribute calls refresh_from_db() method of django.db.models.Model. There is a line db_instance = db_instance_qs.get() in refresh_from_db(), and this line calls __init__() method of the instance recursively.

So, it is necessary to add ensuring that the target attributes are not deferred.

def __init__(self, *args, **kwargs): super(MyClass, self).__init__(*args, **kwargs) deferred_fields = self.get_deferred_fields() important_fields = ['target_type', 'target_id', 'target_object', 'number', 'chain', 'expiration_date'] self.__important_fields = list(filter(lambda x: x not in deferred_fields, important_fields)) for field in self.__important_fields: setattr(self, '__original_%s' % field, getattr(self, field)) 

3 Comments

Recently I have fallen into this problem. Thanks to @Youngkwang
One more thing, what if I want select_related field then?
init is not recommended to store original values, even your approach is elaborated. Use from_db instead. See django discussion about this: forum.djangoproject.com/t/…
5

Something like this also works:

class MyModel(models.Model): my_field = fields.IntegerField() def save(self, *args, **kwargs): # Compare old vs new if self.pk: obj = MyModel.objects.values('my_value').get(pk=self.pk) if obj['my_value'] != self.my_value: # Do stuff... pass super().save(*args, **kwargs) 

3 Comments

Performing a lookup prior to every save doesn't seem very performant.
"Performing a lookup prior to every save doesn't seem very performant" I agree. But it depends on the context. In any case, what do you propose?
@IanE I have added an answer which avoids a DB lookup stackoverflow.com/a/64116052/3446669
4

My use case for this was that I needed to set a denormalized value in the model whenever some field changed its value. However, as the field being monitored was a m2m relation, I didn't want to have to do that DB lookup whenever save was called in order to check whether the denormalized field needed updating. So, instead I wrote this little mixin (using @Odif Yitsaeb's answer as inspiration) in order to only update the denormalized field when necessary.

class HasChangedMixin(object): """ this mixin gives subclasses the ability to set fields for which they want to monitor if the field value changes """ monitor_fields = [] def __init__(self, *args, **kwargs): super(HasChangedMixin, self).__init__(*args, **kwargs) self.field_trackers = {} def __setattr__(self, key, value): super(HasChangedMixin, self).__setattr__(key, value) if key in self.monitor_fields and key not in self.field_trackers: self.field_trackers[key] = value def changed_fields(self): """ :return: `list` of `str` the names of all monitor_fields which have changed """ changed_fields = [] for field, initial_field_val in self.field_trackers.items(): if getattr(self, field) != initial_field_val: changed_fields.append(field) return changed_fields 

1 Comment

Loving this implementation, efficient and simple to add to any model which may required this :)
2

Here is an app that gives you access to previous and current value of a field right before model will be saved: django-smartfields

Here is how this problem can be solved in a nice declarative may:

from django.db import models from smartfields import fields, processors from smartfields.dependencies import Dependency class ConditionalProcessor(processors.BaseProcessor): def process(self, value, stashed_value=None, **kwargs): if value != stashed_value: # do any necessary modifications to new value value = ... return value class MyModel(models.Model): my_field = fields.CharField(max_length=10, dependencies=[ Dependency(processor=ConditionalProcessor()) ]) 

Moreover, this processor will be invoked, only in case that field's value was replaced

Comments

2

I agree with Sahil that it is better and easier to do this with ModelForm. However, you would customize the ModelForm's clean method and perform validation there. In my case, I wanted to prevent updates to a model's instance if a field on the model is set.

My code looked like this:

from django.forms import ModelForm class ExampleForm(ModelForm): def clean(self): cleaned_data = super(ExampleForm, self).clean() if self.instance.field: raise Exception return cleaned_data 

Comments

2

Another way to achieve this is using the post_init and post_save signals to store the initial state of the model.

@receiver(models.signals.post_init) @receiver(models.signals.post_save) def _set_initial_state( sender: Type[Any], instance: Optional[models.Model] = None, **kwargs: Any, ) -> None: """ Store the initial state of the model """ if isinstance(instance, MyModel): instance._initial_state = instance.state 

Where state is the name of a field in MyModel, with _initial_state being the initial version, copied when the modal is initialised/saved.

Be aware if state is a container type (e.g. a dict), you may wish to use deepcopy as appropriate.

6 Comments

I just tried this approach, but I got an error saying that instance has no state attribute. Did you mean instance._state? Either way, how do you then access the initial field values? instance._state doesn't appear to store these.
state is the name of the variable you wish to save. _initial_state is the saved copy. Replace with whatever variable name is appropriate.
I get an error with your instance._initial_state = instance.state because instance.state does not exist. I get an error saying "instance has no state attribute".
Is state a field in your model?
Correct, state is the name of a field in your model.
|
0

Here is how I do. comparing field 'state' for example. and checking permission against user.

admin.py

 def save_model(self, request, obj, form, change): if change is False: obj.created_by = request.user else: # check if field_1 is being updated if obj._loaded_values['state'] != obj.state and not request.user.has_perm('mtasks.change_status', obj): messages.set_level(request, messages.ERROR) messages.error(request, "You don't have permission to change state") return super().save_model(request, obj, form, change) 

in models.py

class ClassName ... @classmethod def from_db(cls, db, field_names, values): instance = super().from_db(db, field_names, values) # save original values, when model is loaded from database, instance._loaded_values = dict(zip(field_names, values)) return instance 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.