Login

Model Mixin to Save Only Changed Fields

Author:
karanlyons
Posted:
August 25, 2013
Language:
Python
Version:
1.5
Tags:
model save mixin class automatic update_fields
Score:
1 (after 1 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. Ordered items in the database - alternative by Leonidas 7 years, 10 months ago
  2. Test runner that installs 'tests' packages as apps by adrian_lc 1 year, 6 months ago
  3. Improved Pickled Object Field by taavi223 5 years, 7 months ago
  4. "Approved" field with timestamp by miracle2k 7 years, 8 months ago
  5. Custom model field to store dict object in database by rudyryk 4 years, 11 months ago

Comments

Please login first before commenting.