diff --git a/django/db/models/query.py b/django/db/models/query.py index cd153a49dc..136b6efbc3 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -255,6 +255,17 @@ class _QuerySet(object): self._result_cache = None delete.alters_data = True + def update(self, **kwargs): + """ + Updates all elements in the current QuerySet, setting all the given + fields to the appropriate values. + """ + query = self.query.clone(sql.UpdateQuery) + query.add_update_values(kwargs) + query.execute_sql(None) + self._result_cache=None + update.alters_Data = True + ################################################## # PUBLIC METHODS THAT RETURN A QUERYSET SUBCLASS # ################################################## diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index ee76523c3b..2f50fdfc98 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -174,6 +174,8 @@ class Query(object): obj.extra_params = self.extra_params[:] obj.extra_order_by = self.extra_order_by[:] obj.__dict__.update(kwargs) + if hasattr(obj, '_setup_query'): + obj._setup_query() return obj def results_iter(self): @@ -1159,6 +1161,12 @@ class UpdateQuery(Query): """ def __init__(self, *args, **kwargs): super(UpdateQuery, self).__init__(*args, **kwargs) + self._setup_query() + + def _setup_query(self): + """ + Run on initialisation and after cloning. + """ self.values = [] def as_sql(self): @@ -1166,22 +1174,24 @@ class UpdateQuery(Query): Creates the SQL for this query. Returns the SQL string and list of parameters. """ - assert len(self.tables) == 1, \ - "Can only update one table at a time." + self.select_related = False + self.pre_sql_setup() + if len(tables) != 1: + raise TypeError('Updates can only access a single database table at a time.') result = ['UPDATE %s' % self.tables[0]] result.append('SET') qn = self.quote_name_unless_alias - values = ['%s = %s' % (qn(v[0]), v[1]) for v in self.values] + values, update_params = [], [] + for name, val in self.values: + if val is not None: + values.append('%s = %%s' % qn(name)) + update_params.append(val) + else: + values.append('%s = NULL' % qn(name)) result.append(', '.join(values)) where, params = self.where.as_sql() result.append('WHERE %s' % where) - return ' '.join(result), tuple(params) - - def do_query(self, table, values, where): - self.tables = [table] - self.values = values - self.where = where - self.execute_sql(NONE) + return ' '.join(result), tuple(update_params + params) def clear_related(self, related_field, pk_list): """ @@ -1191,13 +1201,24 @@ class UpdateQuery(Query): This is used by the QuerySet.delete_objects() method. """ for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): - where = self.where_class() + self.where = self.where_class() f = self.model._meta.pk - where.add((None, f.column, f, 'in', + self.where.add((None, f.column, f, 'in', pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), AND) - values = [(related_field.column, 'NULL')] - self.do_query(self.model._meta.db_table, values, where) + self.values = [(related_field.column, None)] + self.execute_sql(None) + + def add_update_values(self, values): + from django.db.models.base import Model + for name, val in values.items(): + field, direct, m2m = self.model._meta.get_field_by_name(name) + if not direct or m2m: + # Can only update non-relation fields and foreign keys. + raise TypeError('Cannot update model field %r (only non-relations and foreign keys permitted).' % field) + if field.rel and isinstance(val, Model): + val = val.pk + self.values.append((field.column, val)) class DateQuery(Query): """ diff --git a/docs/db-api.txt b/docs/db-api.txt index 342f635219..c37ebd083a 100644 --- a/docs/db-api.txt +++ b/docs/db-api.txt @@ -1972,6 +1972,34 @@ complete query set:: Entry.objects.all().delete() +Updating multiple objects at once +================================= + +**New in Django development version** + +Sometimes you want to set a field to a particular value for all the objects in +a queryset. You can do this with the ``update()`` method. For example:: + + # Update all the headlings to the same value. + Entry.objects.all().update(headline='Everything is the same') + +You can only set non-relation fields and ``ForeignKey`` fields using this +method and the value you set the field to must be a normal Python value (you +can't set a field to be equal to some other field at the moment). + +To update ``ForeignKey`` fields, set the new value to be the new model +instance you want to point to. Example:: + + b = Blog.objects.get(pk=1) + # Make all entries belong to this blog. + Entry.objects.all().update(blog=b) + +The ``update()`` method is applied instantly and doesn't return anything +(similar to ``delete()``). The only restriction on the queryset that is +updated is that it can only access one database table, the model's main +table. So don't try to filter based on related fields or anything like that; +it won't work. + Extra instance methods ====================== diff --git a/tests/modeltests/update/__init__.py b/tests/modeltests/update/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/modeltests/update/models.py b/tests/modeltests/update/models.py new file mode 100644 index 0000000000..55b30cd3f5 --- /dev/null +++ b/tests/modeltests/update/models.py @@ -0,0 +1,60 @@ +""" +Tests for the update() queryset method that allows in-place, multi-object +updates. +""" + +from django.db import models + +class DataPoint(models.Model): + name = models.CharField(max_length=20) + value = models.CharField(max_length=20) + another_value = models.CharField(max_length=20, blank=True) + + def __unicode__(self): + return unicode(self.name) + +class RelatedPoint(models.Model): + name = models.CharField(max_length=20) + data = models.ForeignKey(DataPoint) + + def __unicode__(self): + return unicode(self.name) + + +__test__ = {'API_TESTS': """ +>>> DataPoint(name="d0", value="apple").save() +>>> DataPoint(name="d2", value="banana").save() +>>> d3 = DataPoint(name="d3", value="banana") +>>> d3.save() +>>> RelatedPoint(name="r1", data=d3).save() + +Objects are updated by first filtering the candidates into a queryset and then +calling the update() method. It executes immediately and returns nothing. + +>>> DataPoint.objects.filter(value="apple").update(name="d1") +>>> DataPoint.objects.filter(value="apple") +[] + +We can update multiple objects at once. + +>>> DataPoint.objects.filter(value="banana").update(value="pineapple") +>>> DataPoint.objects.get(name="d2").value +u'pineapple' + +Foreign key fields can also be updated, although you can only update the object +referred to, not anything inside the related object. + +>>> d = DataPoint.objects.get(name="d1") +>>> RelatedPoint.objects.filter(name="r1").update(data=d) +>>> RelatedPoint.objects.filter(data__name="d1") +[] + +Multiple fields can be updated at once + +>>> DataPoint.objects.filter(value="pineapple").update(value="fruit", another_value="peaches") +>>> d = DataPoint.objects.get(name="d2") +>>> d.value, d.another_value +(u'fruit', u'peaches') + +""" +}