Ticket #4102: update_fields_django-1.5-3.patch

File update_fields_django-1.5-3.patch, 17.3 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..aa12adf 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
     472            update_fields = ImmutableSet(update_fields)
     473            field_names = set([field.name for field in self._meta.fields if not field.primary_key])
     474            not_model_fields = update_fields.difference(field_names)
     475
     476            if not_model_fields:
     477                raise ValueError("The following fields do not exist in this model or is m2n field: %s"
     478                                 % ', '.join(not_model_fields))
     479
     480        self.save_base(using=using, force_insert=force_insert,
     481                       force_update=force_update, update_fields=update_fields)
    465482    save.alters_data = True
    466483
    467484    def save_base(self, raw=False, cls=None, origin=None, force_insert=False,
    468             force_update=False, using=None):
     485                  force_update=False, using=None, update_fields=None):
    469486        """
    470487        Does the heavy-lifting involved in saving. Subclasses shouldn't need to
    471488        override this method. It's separate from save() in order to hide the
    class Model(object):  
    473490        ('raw', 'cls', and 'origin').
    474491        """
    475492        using = using or router.db_for_write(self.__class__, instance=self)
    476         assert not (force_insert and force_update)
     493        assert not (force_insert and (force_update or update_fields))
     494        assert not update_fields or len(update_fields) > 0
    477495        if cls is None:
    478496            cls = self.__class__
    479497            meta = cls._meta
    class Model(object):  
    483501            meta = cls._meta
    484502
    485503        if origin and not meta.auto_created:
    486             signals.pre_save.send(sender=origin, instance=self, raw=raw, using=using)
     504            signals.pre_save.send(sender=origin, instance=self, raw=raw, using=using,
     505                                  update_fields=update_fields)
    487506
    488507        # If we are in a raw save, save the object exactly as presented.
    489508        # That means that we don't try to be smart about saving attributes
    class Model(object):  
    503522                if field and getattr(self, parent._meta.pk.attname) is None and getattr(self, field.attname) is not None:
    504523                    setattr(self, parent._meta.pk.attname, getattr(self, field.attname))
    505524
    506                 self.save_base(cls=parent, origin=org, using=using)
     525                self.save_base(cls=parent, origin=org, using=using, update_fields=update_fields)
    507526
    508527                if field:
    509528                    setattr(self, field.attname, self._get_pk_val(parent._meta))
    class Model(object):  
    513532        if not meta.proxy:
    514533            non_pks = [f for f in meta.local_fields if not f.primary_key]
    515534
     535            if update_fields:
     536                non_pks = [f for f in non_pks if f.name in update_fields]
     537
    516538            # First, try an UPDATE. If that doesn't update anything, do an INSERT.
    517539            pk_val = self._get_pk_val(meta)
    518540            pk_set = pk_val is not None
    class Model(object):  
    520542            manager = cls._base_manager
    521543            if pk_set:
    522544                # Determine whether a record with the primary key already exists.
    523                 if (force_update or (not force_insert and
     545                if ((force_update or update_fields) or (not force_insert and
    524546                        manager.using(using).filter(pk=pk_val).exists())):
    525547                    # It does already exist, so do an UPDATE.
    526548                    if force_update or non_pks:
    class Model(object):  
    529551                            rows = manager.using(using).filter(pk=pk_val)._update(values)
    530552                            if force_update and not rows:
    531553                                raise DatabaseError("Forced update did not affect any rows.")
     554                            if update_fields and not rows:
     555                                raise DatabaseError("Save with update_fields did not affect any rows.")
    532556                else:
    533557                    record_exists = False
    534558            if not pk_set or not record_exists:
    class Model(object):  
    541565
    542566                fields = meta.local_fields
    543567                if not pk_set:
    544                     if force_update:
     568                    if force_update or update_fields:
    545569                        raise ValueError("Cannot force an update in save() with no primary key.")
    546570                    fields = [f for f in fields if not isinstance(f, AutoField)]
    547571
    class Model(object):  
    561585
    562586        # Signal that the save is complete
    563587        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)
     588            signals.post_save.send(sender=origin, instance=self, created=(not record_exists),
     589                                   update_fields=update_fields, raw=raw, using=using)
    566590
    567591
    568592    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/models.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..e69de29
    diff --git a/tests/modeltests/update_only_fields/models.py b/tests/modeltests/update_only_fields/models.py
    new file mode 100644
    index 0000000..968dba9
    - +  
     1
     2from django.db import models
     3
     4GENDER_CHOICES = (
     5    ('M', 'Male'),
     6    ('F', 'Female'),
     7)
     8
     9class Account(models.Model):
     10    num = models.IntegerField()
     11
     12
     13class 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
     21class 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
     27class 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
     35class ProxyEmployee(Employee):
     36    class Meta:
     37        proxy = True
  • 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..e1fcc97
    - +  
     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, ProxyEmployee, Profile, Account
     6
     7
     8class 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_m2n(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'])
Back to Top