Created
February 16, 2012 18:17
-
-
Save jacobian/1846830 to your computer and use it in GitHub Desktop.
Save only changed fields when calling Model.save()
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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