Login

Model Mixin to Save Only Changed Fields

Author:
karanlyons
Posted:
August 25, 2013
Language:
Python
Version:
1.5
Score:
2 (after 2 ratings)

Improved and Released as Save The Change.

Django 1.5 added the update_fields kwarg to Model.save(), which allows the developer to specify that only certain fields should actually be committed to the database. However, Django provides no way to automatically commit only changed fields if they're not specified.

This mixin keeps track of which fields have changed from their value in the database, and automatically applies update_fields to update only those fields.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
from copy import copy

from django.db.models import ManyToManyField
from django.db.models.related import RelatedObject


class DoesNotExist:
	"""
	It's unlikely, but there could potentially be a time when a field is added
	to or removed from an instance. We need some representation for those cases.
	
	"""
	
	pass


class SaveOnlyChangedFields(object):
	"""
	Keeps track of fields that have changed since model instantiation, and on
	save updates only those fields.
	
	If save is called with update_fields, the passed kwarg is given precedence.
	
	A caveat: This can't do anything to help you with ManyToManyFields nor
	reverse relationships, which is par for the course: they aren't handled by
	save(), but are pushed to the db immediately on change.
	
	"""
	
	def __init__(self, *args, **kwargs):
		super(SaveOnlyChangedFields, self).__init__(*args, **kwargs)
		
		self._changed_fields = {}
	
	def __setattr__(self, name, value):
		if hasattr(self, '_changed_fields'):
			try:
				name_map = self._meta._name_map
			
			except AttributeError:
				name_map = self._meta.init_name_map()
			
			if name in name_map and name_map[name][0].__class__ not in {ManyToManyField, RelatedObject}:
				old = getattr(self, name, DoesNotExist)
				super(SaveOnlyChangedFields, self).__setattr__(name, value) # A parent's __setattr__ may change value.
				new = getattr(self, name, DoesNotExist)
				
				if old != new:
					changed_fields = self._changed_fields
					
					if name in changed_fields:
						if changed_fields[name] == new:
							# We've changed this field back to its value in the db. No need to push it back up.
							changed_fields.pop(name)
					
					else:
						changed_fields[name] = copy(old)
			
			else:
				super(SaveOnlyChangedFields, self).__setattr__(name, value)
		
		else:
			super(SaveOnlyChangedFields, self).__setattr__(name, value)
	
	def save(self, *args, **kwargs):
		if self._changed_fields and 'update_fields' not in kwargs and not kwargs.get('force_insert', False):
			kwargs['update_fields'] = [key for key, value in self._changed_fields.iteritems() if value is not DoesNotExist]
		
		super(SaveOnlyChangedFields, self).save(*args, **kwargs)
		
		self._changed_fields = {}

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 4 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 7 months ago

Comments

Please login first before commenting.