Browse files

Added the ability to force an SQL insert (or force an update) via a m…


save() method.

git-svn-id: bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
malcolmt committed Aug 9, 2008
1 parent f53e4d8 commit dc14b29fb3e25299736dfb7dcac0c7e6c5b6126d
@@ -17,7 +17,7 @@
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 @@ def _set_pk_val(self, value):
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 @@ def save_base(self, raw=False, cls=None):
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.")
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)]
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]
@@ -399,9 +399,10 @@ def update(self, **kwargs):
"Cannot update a query once a slice has been taken."
query = self.query.clone(sql.UpdateQuery)
- query.execute_sql(None)
+ rows = query.execute_sql(None)
self._result_cache = None
+ return rows
update.alters_data = True
def _update(self, values):
@@ -415,8 +416,8 @@ def _update(self, values):
"Cannot update a query once a slice has been taken."
query = self.query.clone(sql.UpdateQuery)
- query.execute_sql(None)
self._result_cache = None
+ return query.execute_sql(None)
_update.alters_data = True
@@ -109,9 +109,17 @@ def clone(self, klass=None, **kwargs):
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():
+ return rows
def as_sql(self):
@@ -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`_
+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
Retrieving objects
@@ -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
+# Same thing, via an update
+>>> c.value = 3
+# Won't work because force_update and force_insert are mutually exclusive
+>>> c.value = 4
+>>>, 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)
+Traceback (most recent call last):
+ValueError: Cannot force an update in save() with no primary key.
+# Won't work because we can't insert a pk of the same value.
+>>> c.value = 5
+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)
+Traceback (most recent call last):
+DatabaseError: ...

0 comments on commit dc14b29

Please sign in to comment.