diff --git a/django/db/models/base.py b/django/db/models/base.py index 59a503ff82..ed061e6634 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -17,7 +17,7 @@ from django.db.models.fields import AutoField from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField from django.db.models.query import delete_objects, Q, CollectedObjects from django.db.models.options import Options -from django.db import connection, transaction +from django.db import connection, transaction, DatabaseError from django.db.models import signals from django.db.models.loading import register_models, get_model from django.utils.functional import curry @@ -268,22 +268,31 @@ class Model(object): pk = property(_get_pk_val, _set_pk_val) - def save(self): + def save(self, force_insert=False, force_update=False): """ Saves the current instance. Override this in a subclass if you want to control the saving process. + + The 'force_insert' and 'force_update' parameters can be used to insist + that the "save" must be an SQL insert or update (or equivalent for + non-SQL backends), respectively. Normally, they should not be set. """ - self.save_base() + if force_insert and force_update: + raise ValueError("Cannot force both insert and updating in " + "model saving.") + self.save_base(force_insert=force_insert, force_update=force_update) save.alters_data = True - def save_base(self, raw=False, cls=None): + def save_base(self, raw=False, cls=None, force_insert=False, + force_update=False): """ Does the heavy-lifting involved in saving. Subclasses shouldn't need to override this method. It's separate from save() in order to hide the need for overrides of save() to pass around internal-only parameters ('raw' and 'cls'). """ + assert not (force_insert and force_update) if not cls: cls = self.__class__ meta = self._meta @@ -319,15 +328,20 @@ class Model(object): manager = cls._default_manager if pk_set: # Determine whether a record with the primary key already exists. - if manager.filter(pk=pk_val).extra(select={'a': 1}).values('a').order_by(): + if (force_update or (not force_insert and + manager.filter(pk=pk_val).extra(select={'a': 1}).values('a').order_by())): # It does already exist, so do an UPDATE. - if non_pks: + if force_update or non_pks: values = [(f, None, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, False))) for f in non_pks] - manager.filter(pk=pk_val)._update(values) + rows = manager.filter(pk=pk_val)._update(values) + if force_update and not rows: + raise DatabaseError("Forced update did not affect any rows.") else: record_exists = False if not pk_set or not record_exists: if not pk_set: + if force_update: + raise ValueError("Cannot force an update in save() with no primary key.") values = [(f, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in meta.local_fields if not isinstance(f, AutoField)] else: values = [(f, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in meta.local_fields] diff --git a/django/db/models/query.py b/django/db/models/query.py index e97e28b632..14d89dacae 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -399,9 +399,10 @@ class QuerySet(object): "Cannot update a query once a slice has been taken." query = self.query.clone(sql.UpdateQuery) query.add_update_values(kwargs) - query.execute_sql(None) + rows = query.execute_sql(None) transaction.commit_unless_managed() self._result_cache = None + return rows update.alters_data = True def _update(self, values): @@ -415,8 +416,8 @@ class QuerySet(object): "Cannot update a query once a slice has been taken." query = self.query.clone(sql.UpdateQuery) query.add_update_fields(values) - query.execute_sql(None) self._result_cache = None + return query.execute_sql(None) _update.alters_data = True ################################################## diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index 0bb741d706..5ca041cbde 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -109,9 +109,17 @@ class UpdateQuery(Query): related_updates=self.related_updates.copy, **kwargs) def execute_sql(self, result_type=None): - super(UpdateQuery, self).execute_sql(result_type) + """ + Execute the specified update. Returns the number of rows affected by + the primary update query (there could be other updates on related + tables, but their rowcounts are not returned). + """ + cursor = super(UpdateQuery, self).execute_sql(result_type) + rows = cursor.rowcount + del cursor for query in self.get_related_updates(): query.execute_sql(result_type) + return rows def as_sql(self): """ diff --git a/docs/db-api.txt b/docs/db-api.txt index 7e6406f334..bbbe06ea98 100644 --- a/docs/db-api.txt +++ b/docs/db-api.txt @@ -213,8 +213,26 @@ follows this algorithm: The one gotcha here is that you should be careful not to specify a primary-key value explicitly when saving new objects, if you cannot guarantee the -primary-key value is unused. For more on this nuance, see -"Explicitly specifying auto-primary-key values" above. +primary-key value is unused. For more on this nuance, see `Explicitly +specifying auto-primary-key values`_ above and `Forcing an INSERT or UPDATE`_ +below. + +Forcing an INSERT or UPDATE +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**New in Django development version** + +In some rare circumstances, it's necesary to be able to force the ``save()`` +method to perform an SQL ``INSERT`` and not fall back to doing an ``UPDATE``. +Or vice-versa: update, if possible, but not insert a new row. In these cases +you can pass the ``force_insert=True`` or ``force_update=True`` parameters to +the ``save()`` method. Passing both parameters is an error, since you cannot +both insert *and* update at the same time. + +It should be very rare that you'll need to use these parameters. Django will +almost always do the right thing and trying to override that will lead to +errors that are difficult to track down. This feature is for advanced use +only. Retrieving objects ================== diff --git a/tests/modeltests/force_insert_update/__init__.py b/tests/modeltests/force_insert_update/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/modeltests/force_insert_update/models.py b/tests/modeltests/force_insert_update/models.py new file mode 100644 index 0000000000..feffed5faf --- /dev/null +++ b/tests/modeltests/force_insert_update/models.py @@ -0,0 +1,62 @@ +""" +Tests for forcing insert and update queries (instead of Django's normal +automatic behaviour). +""" +from django.db import models + +class Counter(models.Model): + name = models.CharField(max_length = 10) + value = models.IntegerField() + +class WithCustomPK(models.Model): + name = models.IntegerField(primary_key=True) + value = models.IntegerField() + +__test__ = {"API_TESTS": """ +>>> c = Counter.objects.create(name="one", value=1) + +# The normal case +>>> c.value = 2 +>>> c.save() + +# Same thing, via an update +>>> c.value = 3 +>>> c.save(force_update=True) + +# Won't work because force_update and force_insert are mutually exclusive +>>> c.value = 4 +>>> c.save(force_insert=True, force_update=True) +Traceback (most recent call last): +... +ValueError: Cannot force both insert and updating in model saving. + +# Try to update something that doesn't have a primary key in the first place. +>>> c1 = Counter(name="two", value=2) +>>> c1.save(force_update=True) +Traceback (most recent call last): +... +ValueError: Cannot force an update in save() with no primary key. + +>>> c1.save(force_insert=True) + +# Won't work because we can't insert a pk of the same value. +>>> c.value = 5 +>>> c.save(force_insert=True) +Traceback (most recent call last): +... +IntegrityError: ... + +# Work around transaction failure cleaning up for PostgreSQL. +>>> from django.db import connection +>>> connection.close() + +# Trying to update should still fail, even with manual primary keys, if the +# data isn't in the database already. +>>> obj = WithCustomPK(name=1, value=1) +>>> obj.save(force_update=True) +Traceback (most recent call last): +... +DatabaseError: ... + +""" +}