Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #4102 -- Allow update of specific fields in model.save()

Added the ability to update only part of the model's fields in
model.save() by introducing a new kwarg "update_fields". Thanks
to all the numerous reviewers and commenters in the ticket
  • Loading branch information...
commit 365853da016f242937a657b488514e2f69fa6d82 1 parent 2512885
Andrey Antukh authored May 12, 2012 akaariai committed May 12, 2012
55  django/db/models/base.py
@@ -11,7 +11,7 @@
11 11
 from django.db.models.fields import AutoField, FieldDoesNotExist
12 12
 from django.db.models.fields.related import (ManyToOneRel,
13 13
     OneToOneField, add_lazy_relation)
14  
-from django.db import (connections, router, transaction, DatabaseError,
  14
+from django.db import (router, transaction, DatabaseError,
15 15
     DEFAULT_DB_ALIAS)
16 16
 from django.db.models.query import Q
17 17
 from django.db.models.query_utils import DeferredAttribute
@@ -449,7 +449,8 @@ def serializable_value(self, field_name):
449 449
             return getattr(self, field_name)
450 450
         return getattr(self, field.attname)
451 451
 
452  
-    def save(self, force_insert=False, force_update=False, using=None):
  452
+    def save(self, force_insert=False, force_update=False, using=None,
  453
+             update_fields=None):
453 454
         """
454 455
         Saves the current instance. Override this in a subclass if you want to
455 456
         control the saving process.
@@ -458,14 +459,32 @@ def save(self, force_insert=False, force_update=False, using=None):
458 459
         that the "save" must be an SQL insert or update (or equivalent for
459 460
         non-SQL backends), respectively. Normally, they should not be set.
460 461
         """
461  
-        if force_insert and force_update:
  462
+        if force_insert and (force_update or update_fields):
462 463
             raise ValueError("Cannot force both insert and updating in model saving.")
463  
-        self.save_base(using=using, force_insert=force_insert, force_update=force_update)
464 464
 
  465
+        if update_fields is not None:
  466
+            # If update_fields is empty, skip the save. We do also check for
  467
+            # no-op saves later on for inheritance cases. This bailout is
  468
+            # still needed for skipping signal sending.
  469
+            if len(update_fields) == 0:
  470
+                return
  471
+
  472
+            update_fields = frozenset(update_fields)
  473
+            field_names = set([field.name for field in self._meta.fields
  474
+                               if not field.primary_key])
  475
+            non_model_fields = update_fields.difference(field_names)
  476
+
  477
+            if non_model_fields:
  478
+                raise ValueError("The following fields do not exist in this "
  479
+                                 "model or are m2m fields: %s"
  480
+                                 % ', '.join(non_model_fields))
  481
+
  482
+        self.save_base(using=using, force_insert=force_insert,
  483
+                       force_update=force_update, update_fields=update_fields)
465 484
     save.alters_data = True
466 485
 
467 486
     def save_base(self, raw=False, cls=None, origin=None, force_insert=False,
468  
-            force_update=False, using=None):
  487
+                  force_update=False, using=None, update_fields=None):
469 488
         """
470 489
         Does the heavy-lifting involved in saving. Subclasses shouldn't need to
471 490
         override this method. It's separate from save() in order to hide the
@@ -473,7 +492,8 @@ def save_base(self, raw=False, cls=None, origin=None, force_insert=False,
473 492
         ('raw', 'cls', and 'origin').
474 493
         """
475 494
         using = using or router.db_for_write(self.__class__, instance=self)
476  
-        assert not (force_insert and force_update)
  495
+        assert not (force_insert and (force_update or update_fields))
  496
+        assert update_fields is None or len(update_fields) > 0
477 497
         if cls is None:
478 498
             cls = self.__class__
479 499
             meta = cls._meta
@@ -483,7 +503,8 @@ def save_base(self, raw=False, cls=None, origin=None, force_insert=False,
483 503
             meta = cls._meta
484 504
 
485 505
         if origin and not meta.auto_created:
486  
-            signals.pre_save.send(sender=origin, instance=self, raw=raw, using=using)
  506
+            signals.pre_save.send(sender=origin, instance=self, raw=raw, using=using,
  507
+                                  update_fields=update_fields)
487 508
 
488 509
         # If we are in a raw save, save the object exactly as presented.
489 510
         # That means that we don't try to be smart about saving attributes
@@ -503,7 +524,8 @@ def save_base(self, raw=False, cls=None, origin=None, force_insert=False,
503 524
                 if field and getattr(self, parent._meta.pk.attname) is None and getattr(self, field.attname) is not None:
504 525
                     setattr(self, parent._meta.pk.attname, getattr(self, field.attname))
505 526
 
506  
-                self.save_base(cls=parent, origin=org, using=using)
  527
+                self.save_base(cls=parent, origin=org, using=using,
  528
+                               update_fields=update_fields)
507 529
 
508 530
                 if field:
509 531
                     setattr(self, field.attname, self._get_pk_val(parent._meta))
@@ -513,22 +535,27 @@ def save_base(self, raw=False, cls=None, origin=None, force_insert=False,
513 535
         if not meta.proxy:
514 536
             non_pks = [f for f in meta.local_fields if not f.primary_key]
515 537
 
  538
+            if update_fields:
  539
+                non_pks = [f for f in non_pks if f.name in update_fields]
  540
+
516 541
             # First, try an UPDATE. If that doesn't update anything, do an INSERT.
517 542
             pk_val = self._get_pk_val(meta)
518 543
             pk_set = pk_val is not None
519 544
             record_exists = True
520 545
             manager = cls._base_manager
521 546
             if pk_set:
522  
-                # Determine whether a record with the primary key already exists.
523  
-                if (force_update or (not force_insert and
  547
+                # Determine if we should do an update (pk already exists, forced update,
  548
+                # no force_insert)
  549
+                if ((force_update or update_fields) or (not force_insert and
524 550
                         manager.using(using).filter(pk=pk_val).exists())):
525  
-                    # It does already exist, so do an UPDATE.
526 551
                     if force_update or non_pks:
527 552
                         values = [(f, None, (raw and getattr(self, f.attname) or f.pre_save(self, False))) for f in non_pks]
528 553
                         if values:
529 554
                             rows = manager.using(using).filter(pk=pk_val)._update(values)
530 555
                             if force_update and not rows:
531 556
                                 raise DatabaseError("Forced update did not affect any rows.")
  557
+                            if update_fields and not rows:
  558
+                                raise DatabaseError("Save with update_fields did not affect any rows.")
532 559
                 else:
533 560
                     record_exists = False
534 561
             if not pk_set or not record_exists:
@@ -541,7 +568,7 @@ def save_base(self, raw=False, cls=None, origin=None, force_insert=False,
541 568
 
542 569
                 fields = meta.local_fields
543 570
                 if not pk_set:
544  
-                    if force_update:
  571
+                    if force_update or update_fields:
545 572
                         raise ValueError("Cannot force an update in save() with no primary key.")
546 573
                     fields = [f for f in fields if not isinstance(f, AutoField)]
547 574
 
@@ -561,8 +588,8 @@ def save_base(self, raw=False, cls=None, origin=None, force_insert=False,
561 588
 
562 589
         # Signal that the save is complete
563 590
         if origin and not meta.auto_created:
564  
-            signals.post_save.send(sender=origin, instance=self,
565  
-                created=(not record_exists), raw=raw, using=using)
  591
+            signals.post_save.send(sender=origin, instance=self, created=(not record_exists),
  592
+                                   update_fields=update_fields, raw=raw, using=using)
566 593
 
567 594
 
568 595
     save_base.alters_data = True
4  django/db/models/signals.py
@@ -5,8 +5,8 @@
5 5
 pre_init = Signal(providing_args=["instance", "args", "kwargs"])
6 6
 post_init = Signal(providing_args=["instance"])
7 7
 
8  
-pre_save = Signal(providing_args=["instance", "raw", "using"])
9  
-post_save = Signal(providing_args=["instance", "raw", "created", "using"])
  8
+pre_save = Signal(providing_args=["instance", "raw", "using", "update_fields"])
  9
+post_save = Signal(providing_args=["instance", "raw", "created", "using", "update_fields"])
10 10
 
11 11
 pre_delete = Signal(providing_args=["instance", "using"])
12 12
 post_delete = Signal(providing_args=["instance", "using"])
24  docs/ref/models/instances.txt
@@ -135,7 +135,7 @@ Saving objects
135 135
 
136 136
 To save an object back to the database, call ``save()``:
137 137
 
138  
-.. method:: Model.save([force_insert=False, force_update=False, using=DEFAULT_DB_ALIAS])
  138
+.. method:: Model.save([force_insert=False, force_update=False, using=DEFAULT_DB_ALIAS, update_fields=None])
139 139
 
140 140
 .. versionadded:: 1.2
141 141
    The ``using`` argument was added.
@@ -289,6 +289,8 @@ almost always do the right thing and trying to override that will lead to
289 289
 errors that are difficult to track down. This feature is for advanced use
290 290
 only.
291 291
 
  292
+Using ``update_fields`` will force an update similarly to ``force_update``.
  293
+
292 294
 Updating attributes based on existing fields
293 295
 --------------------------------------------
294 296
 
@@ -334,6 +336,26 @@ For more details, see the documentation on :ref:`F() expressions
334 336
 <query-expressions>` and their :ref:`use in update queries
335 337
 <topics-db-queries-update>`.
336 338
 
  339
+Specifying which fields to save
  340
+-------------------------------
  341
+
  342
+.. versionadded:: 1.5
  343
+
  344
+If ``save()`` is passed a list of field names in keyword argument
  345
+``update_fields``, only the fields named in that list will be updated.
  346
+This may be desirable if you want to update just one or a few fields on
  347
+an object. There will be a slight performance benefit from preventing
  348
+all of the model fields from being updated in the database. For example:
  349
+
  350
+    product.name = 'Name changed again'
  351
+    product.save(update_fields=['name'])
  352
+
  353
+The ``update_fields`` argument can be any iterable containing strings. An
  354
+empty ``update_fields`` iterable will skip the save. A value of None will
  355
+perform an update on all fields.
  356
+
  357
+Specifying ``update_fields`` will force an update.
  358
+
337 359
 Deleting objects
338 360
 ================
339 361
 
12  docs/ref/signals.txt
@@ -123,6 +123,12 @@ Arguments sent with this signal:
123 123
 ``using``
124 124
     The database alias being used.
125 125
 
  126
+.. versionadded:: 1.5
  127
+
  128
+``update_fields``
  129
+    The set of fields to update explicitly specified in the ``save()`` method.
  130
+    ``None`` if this argument was not used in the ``save()`` call.
  131
+
126 132
 post_save
127 133
 ---------
128 134
 
@@ -154,6 +160,12 @@ Arguments sent with this signal:
154 160
 ``using``
155 161
     The database alias being used.
156 162
 
  163
+.. versionadded:: 1.5
  164
+
  165
+``update_fields``
  166
+    The set of fields to update explicitly specified in the ``save()`` method.
  167
+    ``None`` if this argument was not used in the ``save()`` call.
  168
+
157 169
 pre_delete
158 170
 ----------
159 171
 
11  docs/releases/1.5.txt
@@ -33,6 +33,17 @@ version compatible with Python 2.6.
33 33
 What's new in Django 1.5
34 34
 ========================
35 35
 
  36
+Support for saving a subset of model's fields
  37
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  38
+
  39
+The method :meth:`Model.save() <django.db.models.Model.save()>` has a new
  40
+keyword argument ``update_fields``. By using this argument it is possible to
  41
+save only a select list of model's fields. This can be useful for performance
  42
+reasons or when trying to avoid overwriting concurrent changes.
  43
+
  44
+See the :meth:`Model.save() <django.db.models.Model.save()>` documentation for
  45
+more details.
  46
+
36 47
 Minor features
37 48
 ~~~~~~~~~~~~~~
38 49
 
0  tests/modeltests/update_only_fields/__init__.py
No changes.
37  tests/modeltests/update_only_fields/models.py
... ...
@@ -0,0 +1,37 @@
  1
+
  2
+from django.db import models
  3
+
  4
+GENDER_CHOICES = (
  5
+    ('M', 'Male'),
  6
+    ('F', 'Female'),
  7
+)
  8
+
  9
+class Account(models.Model):
  10
+    num = models.IntegerField()
  11
+
  12
+
  13
+class Person(models.Model):
  14
+    name = models.CharField(max_length=20)
  15
+    gender = models.CharField(max_length=1, choices=GENDER_CHOICES)
  16
+
  17
+    def __unicode__(self):
  18
+        return self.name
  19
+
  20
+
  21
+class Employee(Person):
  22
+    employee_num = models.IntegerField(default=0)
  23
+    profile = models.ForeignKey('Profile', related_name='profiles', null=True)
  24
+    accounts = models.ManyToManyField('Account', related_name='employees', blank=True, null=True)
  25
+
  26
+
  27
+class Profile(models.Model):
  28
+    name = models.CharField(max_length=200)
  29
+    salary = models.FloatField(default=1000.0)
  30
+
  31
+    def __unicode__(self):
  32
+        return self.name
  33
+
  34
+
  35
+class ProxyEmployee(Employee):
  36
+    class Meta:
  37
+        proxy = True
146  tests/modeltests/update_only_fields/tests.py
... ...
@@ -0,0 +1,146 @@
  1
+from __future__ import absolute_import
  2
+
  3
+from django.test import TestCase
  4
+from django.db.models.signals import pre_save, post_save
  5
+from .models import Person, Employee, ProxyEmployee, Profile, Account
  6
+
  7
+
  8
+class UpdateOnlyFieldsTests(TestCase):
  9
+    def test_update_fields_basic(self):
  10
+        s = Person.objects.create(name='Sara', gender='F')
  11
+        self.assertEqual(s.gender, 'F')
  12
+
  13
+        s.gender = 'M'
  14
+        s.name = 'Ian'
  15
+        s.save(update_fields=['name'])
  16
+
  17
+        s = Person.objects.get(pk=s.pk)
  18
+        self.assertEqual(s.gender, 'F')
  19
+        self.assertEqual(s.name, 'Ian')
  20
+
  21
+    def test_update_fields_m2m(self):
  22
+        profile_boss = Profile.objects.create(name='Boss', salary=3000)
  23
+        e1 = Employee.objects.create(name='Sara', gender='F',
  24
+            employee_num=1, profile=profile_boss)
  25
+
  26
+        a1 = Account.objects.create(num=1)
  27
+        a2 = Account.objects.create(num=2)
  28
+
  29
+        e1.accounts = [a1,a2]
  30
+
  31
+        with self.assertRaises(ValueError):
  32
+            e1.save(update_fields=['accounts'])
  33
+
  34
+    def test_update_fields_inheritance(self):
  35
+        profile_boss = Profile.objects.create(name='Boss', salary=3000)
  36
+        profile_receptionist = Profile.objects.create(name='Receptionist', salary=1000)
  37
+
  38
+        e1 = Employee.objects.create(name='Sara', gender='F',
  39
+            employee_num=1, profile=profile_boss)
  40
+
  41
+        e1.name = 'Ian'
  42
+        e1.gender = 'M'
  43
+        e1.save(update_fields=['name'])
  44
+
  45
+        e2 = Employee.objects.get(pk=e1.pk)
  46
+        self.assertEqual(e2.name, 'Ian')
  47
+        self.assertEqual(e2.gender, 'F')
  48
+        self.assertEqual(e2.profile, profile_boss)
  49
+
  50
+        e2.profile = profile_receptionist
  51
+        e2.name = 'Sara'
  52
+        e2.save(update_fields=['profile'])
  53
+
  54
+        e3 = Employee.objects.get(pk=e1.pk)
  55
+        self.assertEqual(e3.name, 'Ian')
  56
+        self.assertEqual(e3.profile, profile_receptionist)
  57
+
  58
+    def test_update_fields_inheritance_with_proxy_model(self):
  59
+        profile_boss = Profile.objects.create(name='Boss', salary=3000)
  60
+        profile_receptionist = Profile.objects.create(name='Receptionist', salary=1000)
  61
+
  62
+        e1 = ProxyEmployee.objects.create(name='Sara', gender='F',
  63
+            employee_num=1, profile=profile_boss)
  64
+
  65
+        e1.name = 'Ian'
  66
+        e1.gender = 'M'
  67
+        e1.save(update_fields=['name'])
  68
+
  69
+        e2 = ProxyEmployee.objects.get(pk=e1.pk)
  70
+        self.assertEqual(e2.name, 'Ian')
  71
+        self.assertEqual(e2.gender, 'F')
  72
+        self.assertEqual(e2.profile, profile_boss)
  73
+
  74
+        e2.profile = profile_receptionist
  75
+        e2.name = 'Sara'
  76
+        e2.save(update_fields=['profile'])
  77
+
  78
+        e3 = ProxyEmployee.objects.get(pk=e1.pk)
  79
+        self.assertEqual(e3.name, 'Ian')
  80
+        self.assertEqual(e3.profile, profile_receptionist)
  81
+
  82
+    def test_update_fields_signals(self):
  83
+        p = Person.objects.create(name='Sara', gender='F')
  84
+        pre_save_data = []
  85
+        def pre_save_receiver(**kwargs):
  86
+            pre_save_data.append(kwargs['update_fields'])
  87
+        pre_save.connect(pre_save_receiver)
  88
+        post_save_data = []
  89
+        def post_save_receiver(**kwargs):
  90
+            post_save_data.append(kwargs['update_fields'])
  91
+        post_save.connect(post_save_receiver)
  92
+        p.save(update_fields=['name'])
  93
+        self.assertEqual(len(pre_save_data), 1)
  94
+        self.assertEqual(len(pre_save_data[0]), 1)
  95
+        self.assertTrue('name' in pre_save_data[0])
  96
+        self.assertEqual(len(post_save_data), 1)
  97
+        self.assertEqual(len(post_save_data[0]), 1)
  98
+        self.assertTrue('name' in post_save_data[0])
  99
+
  100
+    def test_update_fields_incorrect_params(self):
  101
+        s = Person.objects.create(name='Sara', gender='F')
  102
+
  103
+        with self.assertRaises(ValueError):
  104
+            s.save(update_fields=['first_name'])
  105
+
  106
+        with self.assertRaises(ValueError):
  107
+            s.save(update_fields="name")
  108
+
  109
+    def test_empty_update_fields(self):
  110
+        s = Person.objects.create(name='Sara', gender='F')
  111
+        pre_save_data = []
  112
+        def pre_save_receiver(**kwargs):
  113
+            pre_save_data.append(kwargs['update_fields'])
  114
+        pre_save.connect(pre_save_receiver)
  115
+        post_save_data = []
  116
+        def post_save_receiver(**kwargs):
  117
+            post_save_data.append(kwargs['update_fields'])
  118
+        post_save.connect(post_save_receiver)
  119
+        # Save is skipped.
  120
+        with self.assertNumQueries(0):
  121
+            s.save(update_fields=[])
  122
+        # Signals were skipped, too...
  123
+        self.assertEqual(len(pre_save_data), 0)
  124
+        self.assertEqual(len(post_save_data), 0)
  125
+
  126
+    def test_num_queries_inheritance(self):
  127
+        s = Employee.objects.create(name='Sara', gender='F')
  128
+        s.employee_num = 1
  129
+        s.name = 'Emily'
  130
+        with self.assertNumQueries(1):
  131
+            s.save(update_fields=['employee_num'])
  132
+        s = Employee.objects.get(pk=s.pk)
  133
+        self.assertEqual(s.employee_num, 1)
  134
+        self.assertEqual(s.name, 'Sara')
  135
+        s.employee_num = 2
  136
+        s.name = 'Emily'
  137
+        with self.assertNumQueries(1):
  138
+            s.save(update_fields=['name'])
  139
+        s = Employee.objects.get(pk=s.pk)
  140
+        self.assertEqual(s.name, 'Emily')
  141
+        self.assertEqual(s.employee_num, 1)
  142
+        # A little sanity check that we actually did updates...
  143
+        self.assertEqual(Employee.objects.count(), 1)
  144
+        self.assertEqual(Person.objects.count(), 1)
  145
+        with self.assertNumQueries(2):
  146
+            s.save(update_fields=['name', 'employee_num'])

0 notes on commit 365853d

Please sign in to comment.
Something went wrong with that request. Please try again.