Skip to content

Commit

Permalink
Added the ability to force an SQL insert (or force an update) via a m…
Browse files Browse the repository at this point in the history
…odel's

save() method.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@8267 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information
malcolmt committed Aug 9, 2008
1 parent f53e4d8 commit dc14b29
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 12 deletions.
28 changes: 21 additions & 7 deletions django/db/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
5 changes: 3 additions & 2 deletions django/db/models/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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

##################################################
Expand Down
10 changes: 9 additions & 1 deletion django/db/models/sql/subqueries.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
22 changes: 20 additions & 2 deletions docs/db-api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
==================
Expand Down
Empty file.
62 changes: 62 additions & 0 deletions tests/modeltests/force_insert_update/models.py
Original file line number Diff line number Diff line change
@@ -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: ...
"""
}

0 comments on commit dc14b29

Please sign in to comment.