Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Ticket #4102 - Allow UPDATE of only specific fields in model.save()

  • Loading branch information...
commit f1f36a083a94fe00db55d0b9289ee4717ab2bbe5 1 parent f4cc782
@niwinz niwinz authored
View
35 django/db/models/base.py
@@ -449,7 +449,7 @@ def serializable_value(self, field_name):
return getattr(self, field_name)
return getattr(self, field.attname)
- def save(self, force_insert=False, force_update=False, using=None):
+ def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
"""
Saves the current instance. Override this in a subclass if you want to
control the saving process.
@@ -460,12 +460,29 @@ def save(self, force_insert=False, force_update=False, using=None):
"""
if force_insert and force_update:
raise ValueError("Cannot force both insert and updating in model saving.")
- self.save_base(using=using, force_insert=force_insert, force_update=force_update)
+
+ if update_fields is not None:
+ if not isinstance(update_fields, (list, tuple)):
+ raise ValueError("update_fields must be a list or tuple")
+
+ # if update_fields is empty, does nothink
+ if len(update_fields) == 0:
+ return
+
+ field_names = self._meta.get_all_field_names()
+ for field_name in update_fields:
+ if field_name not in field_names:
+ raise ValueError("%s field does not exist in this model" % (field_name))
+
+ force_update = True
+
+ self.save_base(using=using, force_insert=force_insert,
+ force_update=force_update, update_fields=update_fields)
save.alters_data = True
def save_base(self, raw=False, cls=None, origin=None, force_insert=False,
- force_update=False, using=None):
+ force_update=False, using=None, update_fields=None):
"""
Does the heavy-lifting involved in saving. Subclasses shouldn't need to
override this method. It's separate from save() in order to hide the
@@ -483,7 +500,8 @@ def save_base(self, raw=False, cls=None, origin=None, force_insert=False,
meta = cls._meta
if origin and not meta.auto_created:
- signals.pre_save.send(sender=origin, instance=self, raw=raw, using=using)
+ signals.pre_save.send(sender=origin, instance=self, raw=raw, using=using,
+ update_fields=update_fields)
# If we are in a raw save, save the object exactly as presented.
# That means that we don't try to be smart about saving attributes
@@ -503,7 +521,7 @@ def save_base(self, raw=False, cls=None, origin=None, force_insert=False,
if field and getattr(self, parent._meta.pk.attname) is None and getattr(self, field.attname) is not None:
setattr(self, parent._meta.pk.attname, getattr(self, field.attname))
- self.save_base(cls=parent, origin=org, using=using)
+ self.save_base(cls=parent, origin=org, using=using, update_fields=update_fields)
if field:
setattr(self, field.attname, self._get_pk_val(parent._meta))
@@ -513,6 +531,9 @@ def save_base(self, raw=False, cls=None, origin=None, force_insert=False,
if not meta.proxy:
non_pks = [f for f in meta.local_fields if not f.primary_key]
+ if update_fields:
+ non_pks = [f for f in non_pks if f.name in update_fields]
+
# First, try an UPDATE. If that doesn't update anything, do an INSERT.
pk_val = self._get_pk_val(meta)
pk_set = pk_val is not None
@@ -561,8 +582,8 @@ def save_base(self, raw=False, cls=None, origin=None, force_insert=False,
# Signal that the save is complete
if origin and not meta.auto_created:
- signals.post_save.send(sender=origin, instance=self,
- created=(not record_exists), raw=raw, using=using)
+ signals.post_save.send(sender=origin, instance=self, created=(not record_exists),
+ update_fields=update_fields, raw=raw, using=using)
save_base.alters_data = True
View
4 django/db/models/signals.py
@@ -5,8 +5,8 @@
pre_init = Signal(providing_args=["instance", "args", "kwargs"])
post_init = Signal(providing_args=["instance"])
-pre_save = Signal(providing_args=["instance", "raw", "using"])
-post_save = Signal(providing_args=["instance", "raw", "created", "using"])
+pre_save = Signal(providing_args=["instance", "raw", "using", "update_fields"])
+post_save = Signal(providing_args=["instance", "raw", "created", "using", "update_fields"])
pre_delete = Signal(providing_args=["instance", "using"])
post_delete = Signal(providing_args=["instance", "using"])
View
18 docs/ref/models/instances.txt
@@ -135,7 +135,7 @@ Saving objects
To save an object back to the database, call ``save()``:
-.. method:: Model.save([force_insert=False, force_update=False, using=DEFAULT_DB_ALIAS])
+.. method:: Model.save([force_insert=False, force_update=False, using=DEFAULT_DB_ALIAS, update_fields=None])
.. versionadded:: 1.2
The ``using`` argument was added.
@@ -334,6 +334,22 @@ For more details, see the documentation on :ref:`F() expressions
<query-expressions>` and their :ref:`use in update queries
<topics-db-queries-update>`.
+Specifying which fields to save
+-------------------------------
+
+If ``save()`` is passed a list of field names as keyword argument ``update_fields``,
+only the fields named in that list will be saved to the database. This may be
+desirable if you want to update just one or a few fields on an object, as there
+will be a slight performance benefit from preventing all of the model fields
+from being updated in the database. For example:
+
+ product.name = 'Name changed again'
+ product.save(update_fields=['name'])
+
+
+``update_fields`` must be a list, tuple or None. And this parameter implies ``force_update=True``.
+
+
Deleting objects
================
View
8 docs/ref/signals.txt
@@ -123,6 +123,10 @@ Arguments sent with this signal:
``using``
The database alias being used.
+``update_fields``
+ List or tuple of fields to update explicitly specified in the ``save()`` method.
+ ``None`` if you do not use this parameter when you call ``save()``.
+
post_save
---------
@@ -154,6 +158,10 @@ Arguments sent with this signal:
``using``
The database alias being used.
+``update_fields``
+ List or tuple of fields to update explicitly specified in the ``save()`` method.
+ ``None`` if you do not use this parameter when you call ``save()``.
+
pre_delete
----------
View
3  tests/modeltests/update_only_fields/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+
View
26 tests/modeltests/update_only_fields/models.py
@@ -0,0 +1,26 @@
+
+from django.db import models
+
+GENDER_CHOICES = (
+ ('M', 'Male'),
+ ('F', 'Female'),
+)
+
+class Person(models.Model):
+ name = models.CharField(max_length=20)
+ gender = models.CharField(max_length=1, choices=GENDER_CHOICES)
+
+ def __unicode__(self):
+ return self.name
+
+class Employee(Person):
+ employee_num = models.IntegerField(default=0)
+ profile = models.ForeignKey('Profile', related_name='profiles')
+
+
+class Profile(models.Model):
+ name = models.CharField(max_length=200)
+ salary = models.FloatField(default=1000.0)
+
+ def __unicode__(self):
+ return self.name
View
57 tests/modeltests/update_only_fields/tests.py
@@ -0,0 +1,57 @@
+from __future__ import absolute_import
+from __future__ import with_statement
+
+from django.test import TestCase
+from .models import Person, Employee, Profile
+
+class UpdateOnlyFieldsTests(TestCase):
+ def test_simple_update_fields(self):
+ s = Person.objects.create(name='Sara', gender='F')
+ self.assertEqual(s.gender, 'F')
+
+ s.gender = 'M'
+ s.name = 'Ian'
+ s.save(update_fields=['name'])
+
+ s = Person.objects.get(pk=s.pk)
+ self.assertEqual(s.gender, 'F')
+ self.assertEqual(s.name, 'Ian')
+
+ def test_update_field_with_inherited(self):
+ profile_boss = Profile.objects.create(name='Boss', salary=3000)
+ profile_receptionist = Profile.objects.create(name='Receptionist', salary=1000)
+
+ e1 = Employee.objects.create(name='Sara', gender='F',
+ employee_num=1, profile=profile_boss)
+
+ e1.name = 'Ian'
+ e1.gender = 'M'
+ e1.save(update_fields=['name'])
+
+ e2 = Employee.objects.get(pk=e1.pk)
+ self.assertEqual(e2.name, 'Ian')
+ self.assertEqual(e2.gender, 'F')
+ self.assertEqual(e2.profile, profile_boss)
+
+ e2.profile = profile_receptionist
+ e2.name = 'Sara'
+ e2.save(update_fields=['profile'])
+
+ e3 = Employee.objects.get(pk=e1.pk)
+ self.assertEqual(e3.name, 'Ian')
+ self.assertEqual(e3.profile, profile_receptionist)
+
+ def test_update_field_with_incorrect_params(self):
+ s = Person.objects.create(name='Sara', gender='F')
+
+ with self.assertRaises(ValueError):
+ s.save(update_fields=['first_name'])
+
+ with self.assertRaises(ValueError):
+ s.save(update_fields="name")
+
+ def test_num_querys_on_save_with_empty_update_fields(self):
+ s = Person.objects.create(name='Sara', gender='F')
+
+ with self.assertNumQueries(0):
+ s.save(update_fields=[])
Please sign in to comment.
Something went wrong with that request. Please try again.