Ticket #4102: update_fields_django-1.5-1.patch

File update_fields_django-1.5-1.patch, 15.8 KB (added by Andrei Antoukh, 12 years ago)
  • django/db/models/base.py

    diff --git a/django/db/models/base.py b/django/db/models/base.py
    index 7df4f6d..8bd49f1 100644
    a b import copy  
    22import sys
    33from functools import update_wrapper
    44from itertools import izip
     5from sets import ImmutableSet
    56
    67import django.db.models.manager     # Imported to register signal handler.
    78from django.conf import settings
    from django.core import validators  
    1112from django.db.models.fields import AutoField, FieldDoesNotExist
    1213from django.db.models.fields.related import (ManyToOneRel,
    1314    OneToOneField, add_lazy_relation)
    14 from django.db import (connections, router, transaction, DatabaseError,
     15from django.db import (router, transaction, DatabaseError,
    1516    DEFAULT_DB_ALIAS)
    1617from django.db.models.query import Q
    1718from django.db.models.query_utils import DeferredAttribute
    class Model(object):  
    449450            return getattr(self, field_name)
    450451        return getattr(self, field.attname)
    451452
    452     def save(self, force_insert=False, force_update=False, using=None):
     453    def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
    453454        """
    454455        Saves the current instance. Override this in a subclass if you want to
    455456        control the saving process.
    class Model(object):  
    458459        that the "save" must be an SQL insert or update (or equivalent for
    459460        non-SQL backends), respectively. Normally, they should not be set.
    460461        """
    461         if force_insert and force_update:
     462        if force_insert and (force_update or update_fields):
    462463            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)
    464464
     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            update_fields = ImmutableSet(update_fields)
     472
     473            field_names = set(self._meta.get_all_field_names())
     474            not_model_fields = update_fields.difference(field_names)
     475            if not_model_fields:
     476                raise ValueError("The following fields do not exist in this model: %s"
     477                                 % ', '.join(not_model_fields))
     478
     479        self.save_base(using=using, force_insert=force_insert,
     480                       force_update=force_update, update_fields=update_fields)
    465481    save.alters_data = True
    466482
    467483    def save_base(self, raw=False, cls=None, origin=None, force_insert=False,
    468             force_update=False, using=None):
     484                  force_update=False, using=None, update_fields=None):
    469485        """
    470486        Does the heavy-lifting involved in saving. Subclasses shouldn't need to
    471487        override this method. It's separate from save() in order to hide the
    class Model(object):  
    473489        ('raw', 'cls', and 'origin').
    474490        """
    475491        using = using or router.db_for_write(self.__class__, instance=self)
    476         assert not (force_insert and force_update)
     492        assert not (force_insert and (force_update or update_fields))
     493        assert not update_fields or len(update_fields) > 0
    477494        if cls is None:
    478495            cls = self.__class__
    479496            meta = cls._meta
    class Model(object):  
    483500            meta = cls._meta
    484501
    485502        if origin and not meta.auto_created:
    486             signals.pre_save.send(sender=origin, instance=self, raw=raw, using=using)
     503            signals.pre_save.send(sender=origin, instance=self, raw=raw, using=using,
     504                                  update_fields=update_fields)
    487505
    488506        # If we are in a raw save, save the object exactly as presented.
    489507        # That means that we don't try to be smart about saving attributes
    class Model(object):  
    503521                if field and getattr(self, parent._meta.pk.attname) is None and getattr(self, field.attname) is not None:
    504522                    setattr(self, parent._meta.pk.attname, getattr(self, field.attname))
    505523
    506                 self.save_base(cls=parent, origin=org, using=using)
     524                self.save_base(cls=parent, origin=org, using=using, update_fields=update_fields)
    507525
    508526                if field:
    509527                    setattr(self, field.attname, self._get_pk_val(parent._meta))
    class Model(object):  
    513531        if not meta.proxy:
    514532            non_pks = [f for f in meta.local_fields if not f.primary_key]
    515533
     534            if update_fields:
     535                non_pks = [f for f in non_pks if f.name in update_fields]
     536
    516537            # First, try an UPDATE. If that doesn't update anything, do an INSERT.
    517538            pk_val = self._get_pk_val(meta)
    518539            pk_set = pk_val is not None
    class Model(object):  
    520541            manager = cls._base_manager
    521542            if pk_set:
    522543                # Determine whether a record with the primary key already exists.
    523                 if (force_update or (not force_insert and
     544                if ((force_update or update_fields) or (not force_insert and
    524545                        manager.using(using).filter(pk=pk_val).exists())):
    525546                    # It does already exist, so do an UPDATE.
    526547                    if force_update or non_pks:
    class Model(object):  
    529550                            rows = manager.using(using).filter(pk=pk_val)._update(values)
    530551                            if force_update and not rows:
    531552                                raise DatabaseError("Forced update did not affect any rows.")
     553                            if update_fields and not rows:
     554                                raise DatabaseError("Save with update_fields did not affect any rows.")
    532555                else:
    533556                    record_exists = False
    534557            if not pk_set or not record_exists:
    class Model(object):  
    541564
    542565                fields = meta.local_fields
    543566                if not pk_set:
    544                     if force_update:
     567                    if force_update or update_fields:
    545568                        raise ValueError("Cannot force an update in save() with no primary key.")
    546569                    fields = [f for f in fields if not isinstance(f, AutoField)]
    547570
    class Model(object):  
    561584
    562585        # Signal that the save is complete
    563586        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)
     587            signals.post_save.send(sender=origin, instance=self, created=(not record_exists),
     588                                   update_fields=update_fields, raw=raw, using=using)
    566589
    567590
    568591    save_base.alters_data = True
  • django/db/models/signals.py

    diff --git a/django/db/models/signals.py b/django/db/models/signals.py
    index 48872e7..4666169 100644
    a b class_prepared = Signal(providing_args=["class"])  
    55pre_init = Signal(providing_args=["instance", "args", "kwargs"])
    66post_init = Signal(providing_args=["instance"])
    77
    8 pre_save = Signal(providing_args=["instance", "raw", "using"])
    9 post_save = Signal(providing_args=["instance", "raw", "created", "using"])
     8pre_save = Signal(providing_args=["instance", "raw", "using", "update_fields"])
     9post_save = Signal(providing_args=["instance", "raw", "created", "using", "update_fields"])
    1010
    1111pre_delete = Signal(providing_args=["instance", "using"])
    1212post_delete = Signal(providing_args=["instance", "using"])
  • docs/ref/models/instances.txt

    diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt
    index 3c84be5..c934977 100644
    a b Saving objects  
    135135
    136136To save an object back to the database, call ``save()``:
    137137
    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])
    139139
    140140.. versionadded:: 1.2
    141141   The ``using`` argument was added.
    For more details, see the documentation on :ref:`F() expressions  
    334334<query-expressions>` and their :ref:`use in update queries
    335335<topics-db-queries-update>`.
    336336
     337Specifying which fields to save
     338-------------------------------
     339
     340.. versionadded:: 1.5
     341
     342If ``save()`` is passed a list of field names as keyword argument
     343``update_fields``, only the fields named in that list will be saved to the
     344database. This may be desirable if you want to update just one or a few fields
     345on an object, as there will be a slight performance benefit from preventing
     346all of the model fields from being updated in the database. For example:
     347
     348    product.name = 'Name changed again'
     349    product.save(update_fields=['name'])
     350
     351
     352The ``update_fields`` argument can be any iterable containing strings. An
     353not None, but empty ``update_fields`` skips the save completely (no signals
     354sent). Specifying ``update_fields`` will force an update.
     355
    337356Deleting objects
    338357================
    339358
  • docs/ref/signals.txt

    diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt
    index 3917b52..e047d17 100644
    a b Arguments sent with this signal:  
    123123``using``
    124124    The database alias being used.
    125125
     126``update_fields``
     127    List or tuple of fields to update explicitly specified in the ``save()`` method.
     128    ``None`` if you do not use this parameter when you call ``save()``.
     129
    126130post_save
    127131---------
    128132
    Arguments sent with this signal:  
    154158``using``
    155159    The database alias being used.
    156160
     161``update_fields``
     162    List or tuple of fields to update explicitly specified in the ``save()`` method.
     163    ``None`` if you do not use this parameter when you call ``save()``.
     164
    157165pre_delete
    158166----------
    159167
  • docs/releases/1.5.txt

    diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt
    index 5a92e7d..82e13b9 100644
    a b Django 1.5 also includes several smaller improvements worth noting:  
    4444* :mod:`django.utils.timezone` provides a helper for converting aware
    4545  datetimes between time zones. See :func:`~django.utils.timezone.localtime`.
    4646
     47* :meth:`Model.save() <django.db.models.Model.save()>` has a new argument
     48  ``update_fields``. Using this argument allows restricting the set of fields
     49  to update when saving model instances.
     50
    4751Backwards incompatible changes in 1.5
    4852=====================================
    4953
  • new file tests/modeltests/update_only_fields/__init__.py

    diff --git a/tests/modeltests/update_only_fields/__init__.py b/tests/modeltests/update_only_fields/__init__.py
    new file mode 100644
    index 0000000..faaaf79
    - +  
     1# -*- coding: utf-8 -*-
     2
     3
  • new file tests/modeltests/update_only_fields/models.py

    diff --git a/tests/modeltests/update_only_fields/models.py b/tests/modeltests/update_only_fields/models.py
    new file mode 100644
    index 0000000..3f56ed6
    - +  
     1
     2from django.db import models
     3
     4GENDER_CHOICES = (
     5    ('M', 'Male'),
     6    ('F', 'Female'),
     7)
     8
     9class Person(models.Model):
     10    name = models.CharField(max_length=20)
     11    gender = models.CharField(max_length=1, choices=GENDER_CHOICES)
     12
     13    def __unicode__(self):
     14        return self.name
     15
     16class Employee(Person):
     17    employee_num = models.IntegerField(default=0)
     18    profile = models.ForeignKey('Profile', related_name='profiles', null=True)
     19
     20
     21class Profile(models.Model):
     22    name = models.CharField(max_length=200)
     23    salary = models.FloatField(default=1000.0)
     24
     25    def __unicode__(self):
     26        return self.name
  • new file tests/modeltests/update_only_fields/tests.py

    diff --git a/tests/modeltests/update_only_fields/tests.py b/tests/modeltests/update_only_fields/tests.py
    new file mode 100644
    index 0000000..739ead6
    - +  
     1from __future__ import absolute_import
     2
     3from django.test import TestCase
     4from django.db.models.signals import pre_save, post_save
     5from .models import Person, Employee, Profile
     6
     7class UpdateOnlyFieldsTests(TestCase):
     8    def test_update_fields_basic(self):
     9        s = Person.objects.create(name='Sara', gender='F')
     10        self.assertEqual(s.gender, 'F')
     11
     12        s.gender = 'M'
     13        s.name = 'Ian'
     14        s.save(update_fields=['name'])
     15
     16        s = Person.objects.get(pk=s.pk)
     17        self.assertEqual(s.gender, 'F')
     18        self.assertEqual(s.name, 'Ian')
     19
     20    def test_update_fields_inheritance(self):
     21        profile_boss = Profile.objects.create(name='Boss', salary=3000)
     22        profile_receptionist = Profile.objects.create(name='Receptionist', salary=1000)
     23
     24        e1 = Employee.objects.create(name='Sara', gender='F',
     25            employee_num=1, profile=profile_boss)
     26
     27        e1.name = 'Ian'
     28        e1.gender = 'M'
     29        e1.save(update_fields=['name'])
     30
     31        e2 = Employee.objects.get(pk=e1.pk)
     32        self.assertEqual(e2.name, 'Ian')
     33        self.assertEqual(e2.gender, 'F')
     34        self.assertEqual(e2.profile, profile_boss)
     35
     36        e2.profile = profile_receptionist
     37        e2.name = 'Sara'
     38        e2.save(update_fields=['profile'])
     39
     40        e3 = Employee.objects.get(pk=e1.pk)
     41        self.assertEqual(e3.name, 'Ian')
     42        self.assertEqual(e3.profile, profile_receptionist)
     43
     44    def test_update_fields_signals(self):
     45        p = Person.objects.create(name='Sara', gender='F')
     46        pre_save_data = []
     47        def pre_save_receiver(**kwargs):
     48            pre_save_data.append(kwargs['update_fields'])
     49        pre_save.connect(pre_save_receiver)
     50        post_save_data = []
     51        def post_save_receiver(**kwargs):
     52            post_save_data.append(kwargs['update_fields'])
     53        post_save.connect(post_save_receiver)
     54        p.save(update_fields=['name'])
     55        self.assertEqual(len(pre_save_data), 1)
     56        self.assertEqual(len(pre_save_data[0]), 1)
     57        self.assertTrue('name' in pre_save_data[0])
     58        self.assertEqual(len(post_save_data), 1)
     59        self.assertEqual(len(post_save_data[0]), 1)
     60        self.assertTrue('name' in post_save_data[0])
     61
     62    def test_update_fields_incorrect_params(self):
     63        s = Person.objects.create(name='Sara', gender='F')
     64
     65        with self.assertRaises(ValueError):
     66            s.save(update_fields=['first_name'])
     67
     68        with self.assertRaises(ValueError):
     69            s.save(update_fields="name")
     70
     71    def test_empty_update_fields(self):
     72        s = Person.objects.create(name='Sara', gender='F')
     73        pre_save_data = []
     74        def pre_save_receiver(**kwargs):
     75            pre_save_data.append(kwargs['update_fields'])
     76        pre_save.connect(pre_save_receiver)
     77        post_save_data = []
     78        def post_save_receiver(**kwargs):
     79            post_save_data.append(kwargs['update_fields'])
     80        post_save.connect(post_save_receiver)
     81        # Save is skipped.
     82        with self.assertNumQueries(0):
     83            s.save(update_fields=[])
     84        # Signals were skipped, too...
     85        self.assertEqual(len(pre_save_data), 0)
     86        self.assertEqual(len(post_save_data), 0)
     87
     88    def test_num_queries_inheritance(self):
     89        s = Employee.objects.create(name='Sara', gender='F')
     90        s.employee_num = 1
     91        s.name = 'Emily'
     92        with self.assertNumQueries(1):
     93            s.save(update_fields=['employee_num'])
     94        s = Employee.objects.get(pk=s.pk)
     95        self.assertEqual(s.employee_num, 1)
     96        self.assertEqual(s.name, 'Sara')
     97        s.employee_num = 2
     98        s.name = 'Emily'
     99        with self.assertNumQueries(1):
     100            s.save(update_fields=['name'])
     101        s = Employee.objects.get(pk=s.pk)
     102        self.assertEqual(s.name, 'Emily')
     103        self.assertEqual(s.employee_num, 1)
     104        # A little sanity check that we actually did updates...
     105        self.assertEqual(Employee.objects.count(), 1)
     106        self.assertEqual(Person.objects.count(), 1)
     107        with self.assertNumQueries(2):
     108            s.save(update_fields=['name', 'employee_num'])
Back to Top