Skip to content

Instantly share code, notes, and snippets.

@jacobian
Created February 16, 2012 18:17
Show Gist options
  • Save jacobian/1846830 to your computer and use it in GitHub Desktop.
Save jacobian/1846830 to your computer and use it in GitHub Desktop.
Save only changed fields when calling Model.save()
def saves_only_changes(cls):
"""
When calling save(), only save changed model fields.
This is a class decorator, so use it thusly::
@saves_only_changes
class Person(models.Model):
first_name = models.CharField(max_length=200)
last_name = models.CharField(max_length=200)
bio = models.TextField()
Once done, use the model normally::
>>> p = Person(first_name='Roger', last_name='Waters')
>>> p.save()
SQL: INSERT INTO "testapp_person" ("first_name", "last_name", "bio")
VALUES ('Roger', 'Waters', '');
However, now when you modify fields, calling `save()` will issue an UPDATE
with just the changed fields::
>>> p.first_name = 'John'
>>> p.save()
SQL: UPDATE "testapp_person"
SET "first_name" = 'John' WHERE "testapp_person"."id" = 2;
You can also force particular fields to be saved with a `fields` argument
to `save()`::
>>> p.first_name = 'James'
>>> p.last_name = 'Lennon'
>>> p.save(fields=['last_name'])
SQL: UPDATE "testapp_person"
SET "last_name" = 'Lennon' WHERE "testapp_person"."id" = 2;
>>> Person.objects.all()
[<Person: John Lennon>]
Note that if you don't modify any fields a full update will be issued.
This is a corner case, but it's worth noting::
>>> p = Person.objects.get(last_name="Lennon")
>>> p.save()
SQL: SELECT (1) AS "a" FROM "testapp_person"
WHERE "testapp_person"."id" = 2;
SQL: UPDATE "testapp_person"
SET "first_name" = 'John', "last_name" = 'Lennon', "bio" = ''
WHERE "testapp_person"."id" = 2;
(That's a normal model save behavior right there.)
"""
# We'll want to override (read: monkeypatch) __init__(), __setattr__() and
# save() on the model in question. These are the replacement methods;
# they're hooked up below.
def patched_init(self, *args, **kwargs):
# We want to be a bit clever here: don't set _tracked_fields until after
# __init__ is done to make any setattrs done in __init__ avoid
# triggering the "field changed" hook. And of course remember to hit
# __dict__ to avoid triggering setattr by setting _tracked_fields.
self.__dict__['_tracked_fields'] = frozenset()
super(cls, self).__init__(*args, **kwargs)
self._tracked_fields = frozenset(f.attname for f in self._meta.fields)
self._modified_fields = set()
def patched_setattr(self, name, value):
if name in self._tracked_fields:
self._modified_fields.add(name)
super(cls, self).__setattr__(name, value)
def patched_save(self, *args, **kwargs):
fields = kwargs.pop('fields', self._modified_fields)
if fields:
values = dict((f, getattr(self, f)) for f in fields)
self._base_manager.filter(pk=self.pk).update(**values)
else:
super(cls, self).save(*args, **kwargs)
self._modified_fields.clear()
# PATCH ALL THE MONKEYS!
cls.__init__ = patched_init
cls.__setattr__ = patched_setattr
cls.save = patched_save
return cls
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment