Commit e973ee6a authored by Anssi Kääriäinen's avatar Anssi Kääriäinen
Browse files

Fixed #20988 -- Added model meta option select_on_save

The option can be used to force pre 1.6 style SELECT on save behaviour.
This is needed in case the database returns zero updated rows even if
there is a matching row in the DB. One such case is PostgreSQL update
trigger that returns NULL.

Reviewed by Tim Graham.

Refs #16649
parent 13be3bfe
Loading
Loading
Loading
Loading
+13 −5
Original line number Diff line number Diff line
@@ -667,7 +667,9 @@ class Model(six.with_metaclass(ModelBase)):
            base_qs = cls._base_manager.using(using)
            values = [(f, None, (getattr(self, f.attname) if raw else f.pre_save(self, False)))
                      for f in non_pks]
            updated = self._do_update(base_qs, using, pk_val, values, update_fields)
            forced_update = update_fields or force_update
            updated = self._do_update(base_qs, using, pk_val, values, update_fields,
                                      forced_update)
            if force_update and not updated:
                raise DatabaseError("Forced update did not affect any rows.")
            if update_fields and not updated:
@@ -691,21 +693,27 @@ class Model(six.with_metaclass(ModelBase)):
                setattr(self, meta.pk.attname, result)
        return updated

    def _do_update(self, base_qs, using, pk_val, values, update_fields):
    def _do_update(self, base_qs, using, pk_val, values, update_fields, forced_update):
        """
        This method will try to update the model. If the model was updated (in
        the sense that an update query was done and a matching row was found
        from the DB) the method will return True.
        """
        filtered = base_qs.filter(pk=pk_val)
        if not values:
            # We can end up here when saving a model in inheritance chain where
            # update_fields doesn't target any field in current model. In that
            # case we just say the update succeeded. Another case ending up here
            # is a model with just PK - in that case check that the PK still
            # exists.
            return update_fields is not None or base_qs.filter(pk=pk_val).exists()
            return update_fields is not None or filtered.exists()
        if self._meta.select_on_save and not forced_update:
            if filtered.exists():
                filtered._update(values)
                return True
            else:
            return base_qs.filter(pk=pk_val)._update(values) > 0
                return False
        return filtered._update(values) > 0

    def _do_insert(self, manager, using, fields, update_pk, raw):
        """
+3 −1
Original line number Diff line number Diff line
@@ -22,7 +22,8 @@ DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
                 'unique_together', 'permissions', 'get_latest_by',
                 'order_with_respect_to', 'app_label', 'db_tablespace',
                 'abstract', 'managed', 'proxy', 'swappable', 'auto_created',
                 'index_together', 'app_cache', 'default_permissions')
                 'index_together', 'app_cache', 'default_permissions',
                 'select_on_save')

@python_2_unicode_compatible
class Options(object):
@@ -35,6 +36,7 @@ class Options(object):
        self.ordering = []
        self.unique_together = []
        self.index_together = []
        self.select_on_save = False
        self.default_permissions = ('add', 'change', 'delete')
        self.permissions = []
        self.object_name, self.app_label = None, app_label
+12 −5
Original line number Diff line number Diff line
@@ -305,16 +305,23 @@ follows this algorithm:
* If the object's primary key attribute is *not* set or if the ``UPDATE``
  didn't update anything, Django executes an ``INSERT``.

.. versionchanged:: 1.6

    Previously Django used ``SELECT`` - if not found ``INSERT`` else ``UPDATE``
    algorithm. The old algorithm resulted in one more query in ``UPDATE`` case.

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 and `Forcing an INSERT or UPDATE`_ below.

.. versionchanged:: 1.6

    Previously Django did a ``SELECT`` when the primary key attribute was set.
    If the ``SELECT`` found a row, then Django did an ``UPDATE``, otherwise it
    did an ``INSERT``. The old algorithm results in one more query in the
    ``UPDATE`` case. There are some rare cases where the database doesn't
    report that a row was updated even if the database contains a row for the
    object's primary key value. An example is the PostgreSQL ``ON UPDATE``
    trigger which returns ``NULL``. In such cases it is possible to revert to the
    old algorithm by setting the :attr:`~django.db.models.Options.select_on_save`
    option to ``True``.

.. _ref-models-force-insert:

Forcing an INSERT or UPDATE
+22 −0
Original line number Diff line number Diff line
@@ -256,6 +256,28 @@ Django quotes column and table names behind the scenes.
    If ``proxy = True``, a model which subclasses another model will be treated as
    a :ref:`proxy model <proxy-models>`.

``select_on_save``
------------------

.. attribute:: Options.select_on_save

    .. versionadded:: 1.6

    Determines if Django will use the pre-1.6
    :meth:`django.db.models.Model.save()` algorithm. The old algorithm
    uses ``SELECT`` to determine if there is an existing row to be updated.
    The new algorith tries an ``UPDATE`` directly. In some rare cases the
    ``UPDATE`` of an existing row isn't visible to Django. An example is the
    PostgreSQL ``ON UPDATE`` trigger which returns ``NULL``. In such cases the
    new algorithm will end up doing an ``INSERT`` even when a row exists in
    the database.

    Usually there is no need to set this attribute. The default is
    ``False``.

    See :meth:`django.db.models.Model.save()` for more about the old and
    new saving algorithm.

``unique_together``
-------------------

+16 −4
Original line number Diff line number Diff line
@@ -138,6 +138,22 @@ A :djadmin:`check` management command was added, enabling you to verify if your
current configuration (currently oriented at settings) is compatible with the
current version of Django.

:meth:`Model.save() <django.db.models.Model.save()>` algorithm changed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The :meth:`Model.save() <django.db.models.Model.save()>` method now
tries to directly ``UPDATE`` the database if the instance has a primary
key value. Previously ``SELECT`` was performed to determine if ``UPDATE``
or ``INSERT`` were needed. The new algorithm needs only one query for
updating an existing row while the old algorithm needed two. See
:meth:`Model.save() <django.db.models.Model.save()>` for more details.

In some rare cases the database doesn't report that a matching row was
found when doing an ``UPDATE``. An example is the PostgreSQL ``ON UPDATE``
trigger which returns ``NULL``. In such cases it is possible to set
:attr:`django.db.models.Options.select_on_save` flag to force saving to
use the old algorithm.

Minor features
~~~~~~~~~~~~~~

@@ -222,10 +238,6 @@ Minor features
* Generic :class:`~django.contrib.gis.db.models.GeometryField` is now editable
  with the OpenLayers widget in the admin.

* The :meth:`Model.save() <django.db.models.Model.save()>` will do
  ``UPDATE`` - if not updated - ``INSERT`` instead of ``SELECT`` - if not
  found ``INSERT`` else ``UPDATE`` in case the model's primary key is set.

* The documentation contains a :doc:`deployment checklist
  </howto/deployment/checklist>`.

Loading