Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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

…odel's

save() method.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@8267 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit dc14b29fb3e25299736dfb7dcac0c7e6c5b6126d 1 parent f53e4d8
@malcolmt malcolmt authored
View
28 django/db/models/base.py
@@ -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.")
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]
View
5 django/db/models/query.py
@@ -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.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 @@ def _update(self, values):
"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
##################################################
View
10 django/db/models/sql/subqueries.py
@@ -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():
query.execute_sql(result_type)
+ return rows
def as_sql(self):
"""
View
22 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
==================
View
0  tests/modeltests/force_insert_update/__init__.py
No changes.
View
62 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: ...
+
+"""
+}
Please sign in to comment.
Something went wrong with that request. Please try again.