Kaydet (Commit) 6b483495 authored tarafından Anssi Kääriäinen's avatar Anssi Kääriäinen

Fixed #16649 -- Refactored save_base logic

Model.save() will use UPDATE - if not updated - INSERT instead of
SELECT - if found UPDATE else INSERT. This should save a query when
updating, but will cost a little when inserting model with PK set.

Also fixed #17341 -- made sure .save() commits transactions only after
the whole model has been saved. This wasn't the case in model
inheritance situations.

The save_base implementation was refactored into multiple methods.
A typical chain for inherited save is:
save_base()
    _save_parents(self)
        for each parent:
            _save_parents(parent)
            _save_table(parent)
    _save_table(self)
üst 8a2f5300
This diff is collapsed.
......@@ -292,12 +292,13 @@ follows this algorithm:
* If the object's primary key attribute is set to a value that evaluates to
``True`` (i.e., a value other than ``None`` or the empty string), Django
executes a ``SELECT`` query to determine whether a record with the given
primary key already exists.
* If the record with the given primary key does already exist, Django
executes an ``UPDATE`` query.
* If the object's primary key attribute is *not* set, or if it's set but a
record doesn't exist, Django executes an ``INSERT``.
executes an ``UPDATE``.
* 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
......
......@@ -150,6 +150,10 @@ 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.
Backwards incompatible changes in 1.6
=====================================
......
from __future__ import absolute_import, unicode_literals
from datetime import datetime
import threading
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError
from django.db import connections, DEFAULT_DB_ALIAS
from django.db.models.fields import Field, FieldDoesNotExist
from django.db.models.query import QuerySet, EmptyQuerySet, ValuesListQuerySet
from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
from django.test import TestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature
from django.utils import six
from django.utils.translation import ugettext_lazy
......@@ -690,4 +692,29 @@ class ModelTest(TestCase):
def test_invalid_qs_list(self):
qs = Article.objects.order_by('invalid_column')
self.assertRaises(FieldError, list, qs)
self.assertRaises(FieldError, list, qs)
\ No newline at end of file
self.assertRaises(FieldError, list, qs)
class ConcurrentSaveTests(TransactionTestCase):
@skipUnlessDBFeature('test_db_allows_multiple_connections')
def test_concurrent_delete_with_save(self):
"""
Test fetching, deleting and finally saving an object - we should get
an insert in this case.
"""
a = Article.objects.create(headline='foo', pub_date=datetime.now())
exceptions = []
def deleter():
try:
# Do not delete a directly - doing so alters its state.
Article.objects.filter(pk=a.pk).delete()
connections[DEFAULT_DB_ALIAS].commit_unless_managed()
except Exception as e:
exceptions.append(e)
finally:
connections[DEFAULT_DB_ALIAS].close()
self.assertEqual(len(exceptions), 0)
t = threading.Thread(target=deleter)
t.start()
t.join()
a.save()
self.assertEqual(Article.objects.get(pk=a.pk).headline, 'foo')
......@@ -294,7 +294,7 @@ class ModelInheritanceTests(TestCase):
rating=4,
chef=c
)
with self.assertNumQueries(6):
with self.assertNumQueries(3):
ir.save()
def test_update_parent_filtering(self):
......
......@@ -4,6 +4,8 @@ from django.db import models
class Mod(models.Model):
fld = models.IntegerField()
class SubMod(Mod):
cnt = models.IntegerField(unique=True)
class M2mA(models.Model):
others = models.ManyToManyField('M2mB')
......
from __future__ import absolute_import
from django.db import connection, connections, transaction, DEFAULT_DB_ALIAS, DatabaseError
from django.db import (connection, connections, transaction, DEFAULT_DB_ALIAS, DatabaseError,
IntegrityError)
from django.db.transaction import commit_on_success, commit_manually, TransactionManagementError
from django.test import TransactionTestCase, skipUnlessDBFeature
from django.test.utils import override_settings
......@@ -8,8 +9,25 @@ from django.utils.unittest import skipIf, skipUnless
from transactions.tests import IgnorePendingDeprecationWarningsMixin
from .models import Mod, M2mA, M2mB
from .models import Mod, M2mA, M2mB, SubMod
class ModelInheritanceTests(TransactionTestCase):
def test_save(self):
# First, create a SubMod, then try to save another with conflicting
# cnt field. The problem was that transactions were committed after
# every parent save when not in managed transaction. As the cnt
# conflict is in the second model, we can check if the first save
# was committed or not.
SubMod(fld=1, cnt=1).save()
# We should have committed the transaction for the above - assert this.
connection.rollback()
self.assertEqual(SubMod.objects.count(), 1)
try:
SubMod(fld=2, cnt=1).save()
except IntegrityError:
connection.rollback()
self.assertEqual(SubMod.objects.count(), 1)
self.assertEqual(Mod.objects.count(), 1)
class TestTransactionClosing(IgnorePendingDeprecationWarningsMixin, TransactionTestCase):
"""
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment