Added the ability to force an SQL insert (or force an update) via a model's

save() method.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@8267 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Malcolm Tredinnick 2008-08-09 17:19:23 +00:00
parent f53e4d80b4
commit dc14b29fb3
6 changed files with 115 additions and 12 deletions

View File

@ -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.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
from django.db.models.query import delete_objects, Q, CollectedObjects from django.db.models.query import delete_objects, Q, CollectedObjects
from django.db.models.options import Options 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 import signals
from django.db.models.loading import register_models, get_model from django.db.models.loading import register_models, get_model
from django.utils.functional import curry from django.utils.functional import curry
@ -268,22 +268,31 @@ class Model(object):
pk = property(_get_pk_val, _set_pk_val) 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 Saves the current instance. Override this in a subclass if you want to
control the saving process. 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 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 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 override this method. It's separate from save() in order to hide the
need for overrides of save() to pass around internal-only parameters need for overrides of save() to pass around internal-only parameters
('raw' and 'cls'). ('raw' and 'cls').
""" """
assert not (force_insert and force_update)
if not cls: if not cls:
cls = self.__class__ cls = self.__class__
meta = self._meta meta = self._meta
@ -319,15 +328,20 @@ class Model(object):
manager = cls._default_manager manager = cls._default_manager
if pk_set: if pk_set:
# Determine whether a record with the primary key already exists. # 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. # 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] 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: else:
record_exists = False record_exists = False
if not pk_set or not record_exists: if not pk_set or not record_exists:
if not pk_set: 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 if not isinstance(f, AutoField)]
else: 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] 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 File

@ -399,9 +399,10 @@ class QuerySet(object):
"Cannot update a query once a slice has been taken." "Cannot update a query once a slice has been taken."
query = self.query.clone(sql.UpdateQuery) query = self.query.clone(sql.UpdateQuery)
query.add_update_values(kwargs) query.add_update_values(kwargs)
query.execute_sql(None) rows = query.execute_sql(None)
transaction.commit_unless_managed() transaction.commit_unless_managed()
self._result_cache = None self._result_cache = None
return rows
update.alters_data = True update.alters_data = True
def _update(self, values): def _update(self, values):
@ -415,8 +416,8 @@ class QuerySet(object):
"Cannot update a query once a slice has been taken." "Cannot update a query once a slice has been taken."
query = self.query.clone(sql.UpdateQuery) query = self.query.clone(sql.UpdateQuery)
query.add_update_fields(values) query.add_update_fields(values)
query.execute_sql(None)
self._result_cache = None self._result_cache = None
return query.execute_sql(None)
_update.alters_data = True _update.alters_data = True
################################################## ##################################################

View File

@ -109,9 +109,17 @@ class UpdateQuery(Query):
related_updates=self.related_updates.copy, **kwargs) related_updates=self.related_updates.copy, **kwargs)
def execute_sql(self, result_type=None): 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(): for query in self.get_related_updates():
query.execute_sql(result_type) query.execute_sql(result_type)
return rows
def as_sql(self): def as_sql(self):
""" """

View File

@ -213,8 +213,26 @@ follows this algorithm:
The one gotcha here is that you should be careful not to specify a primary-key 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 value explicitly when saving new objects, if you cannot guarantee the
primary-key value is unused. For more on this nuance, see primary-key value is unused. For more on this nuance, see `Explicitly
"Explicitly specifying auto-primary-key values" above. 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 Retrieving objects
================== ==================

View File

@ -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: ...
"""
}