Code

Ticket #4102: update_fields_django-1.5-3.patch

File update_fields_django-1.5-3.patch, 17.3 KB (added by niwi, 2 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'])