Advanced Usage

Verbose mode

By default, when you use get_dirty_fields() function, if there are dirty fields, only the saved value is returned. You can use the verbose option to return the saved and current in-memory value:

>>> model = ExampleModel.objects.create(characters="first value")
>>> model.characters = "second value"
>>> model.get_dirty_fields()
{'characters': 'first_value'}
>>> model.get_dirty_fields(verbose=True)
{'characters': {'saved': 'first value', 'current': 'second value'}}

Checking foreign key fields.

By default, dirty functions are not checking foreign keys. If you want to also take these relationships into account, use check_relationship parameter:

class ForeignKeyModel(DirtyFieldsMixin, models.Model):
    fkey = models.ForeignKey(AnotherModel, on_delete=models.CASCADE)
>>> model = ForeignKeyModel.objects.create(fkey=obj1)
>>> model.is_dirty()
False
>>> model.fkey = obj2
>>> model.is_dirty()
False
>>> model.is_dirty(check_relationship=True)
True
>>> model.get_dirty_fields()
{}
>>> model.get_dirty_fields(check_relationship=True)
{'fkey': 1}

Saving dirty fields.

If you want to only save dirty fields from an instance in the database (only these fields will be involved in SQL query), you can use save_dirty_fields() method. If the model instance has not been persisted yet, it will be saved in full.

Warning: This calls the save() method internally so will trigger the same signals as normally calling the save() method.

>>> model.is_dirty()
True
>>> model.save_dirty_fields()
>>> model.is_dirty()
False

Performance Impact

Using DirtyFieldsMixin in your Model will have a (normally small) performance impact even when you don’t call any of DirtyFieldsMixin’s methods. This is because DirtyFieldsMixin needs to capture the state of the Model when it is initialized and when it is saved, so that DirtyFieldsMixin can later determine if the fields are dirty.

Using a Proxy Model to reduce Performance Impact

If you only use DirtyFieldsMixin’s methods in some places of you project but not all, you can eliminate the performance impact in the places you don’t use them by inheriting from DirtyFieldsMixin in a Proxy Model.

For example define your Model without DirtyFieldsMixin:

class FooModel(models.Model):
    ...

Use this Model class when you don’t need to track dirty fields. It is a regular Model so there will be no performance impact, but is_dirty() and get_dirty_fields() can’t be used.

Then define a Proxy Model for that Model which includes DirtyFieldsMixin:

class FooModelWithDirtyFields(DirtyFieldsMixin, FooModel):
    class Meta:
        proxy = True

Use this Model class when you do want dirty fields to be tracked. There will be a performance impact but is_dirty() and get_dirty_fields() can be used.

Database Transactions Limitations

There is currently a limitation when using dirtyfields and database transactions. If your code saves Model instances inside a transaction.atomic() block, and the transaction is rolled back, then the Model instance’s is_dirty() method will return False when it should return True. The get_dirty_fields() method will also return the wrong thing in the same way.

This is because after the save() method is called, the instance’s dirty state is reset because it thinks it has successfully saved to the database. Then when the transaction rolls back, the database is reset back to the original value. At this point this Model instance thinks it is not dirty when it actually is. Here is a code example to illustrate the problem:

# first create a model
model = ExampleModel.objects.create(characters="first")
# then make an edit in-memory, model becomes dirty
model.characters = "second"
assert model.is_dirty()
# then attempt to save the model in a transaction
try:
    with transaction.atomic():
        model.save()
        # no longer dirty because save() has been called,
        # BUT we are still in a transaction ...
        assert not model.is_dirty()
        # force a transaction rollback
        raise DatabaseError("pretend something went wrong")
except DatabaseError:
    pass

# Here is the problem:
# value in DB is still "first" but model does not think its dirty,
# because in-memory value is still "second"
assert model.characters == "second"
assert not model.is_dirty()

This simplest workaround to this issue is to call model.refresh_from_db() if the transaction is rolled back. Or you can manually restore the fields that were edited in-memory.