Fixed #31573 -- Made QuerySet.update() respect ordering on MariaDB/MySQL.

This commit is contained in:
davidchorpash 2020-06-19 22:55:03 -06:00 committed by Mariusz Felisiak
parent 060576b0ab
commit 779e615e36
5 changed files with 79 additions and 2 deletions

View File

@ -1,3 +1,4 @@
from django.core.exceptions import FieldError
from django.db.models.sql import compiler
@ -36,7 +37,23 @@ class SQLDeleteCompiler(compiler.SQLDeleteCompiler, SQLCompiler):
class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler):
pass
def as_sql(self):
update_query, update_params = super().as_sql()
# MySQL and MariaDB support UPDATE ... ORDER BY syntax.
if self.query.order_by:
order_by_sql = []
order_by_params = []
try:
for _, (sql, params, _) in self.get_order_by():
order_by_sql.append(sql)
order_by_params.extend(params)
update_query += ' ORDER BY ' + ', '.join(order_by_sql)
update_params += tuple(order_by_params)
except FieldError:
# Ignore ordering if it contains annotations, because they're
# removed in .update() and cannot be resolved.
pass
return update_query, update_params
class SQLAggregateCompiler(compiler.SQLAggregateCompiler, SQLCompiler):

View File

@ -2530,6 +2530,21 @@ update a bunch of records for a model that has a custom
e.comments_on = False
e.save()
Ordered queryset
^^^^^^^^^^^^^^^^
.. versionadded:: 3.2
Chaining ``order_by()`` with ``update()`` is supported only on MariaDB and
MySQL, and is ignored for different databases. This is useful for updating a
unique field in the order that is specified without conflicts. For example::
Entry.objects.order_by('-number').update(number=F('number') + 1)
.. note::
If the ``order_by()`` clause contains annotations, it will be ignored.
``delete()``
~~~~~~~~~~~~

View File

@ -224,6 +224,9 @@ Models
* The new :attr:`.UniqueConstraint.opclasses` attribute allows setting
PostgreSQL operator classes.
* The :meth:`.QuerySet.update` method now respects the ``order_by()`` clause on
MySQL and MariaDB.
Requests and Responses
~~~~~~~~~~~~~~~~~~~~~~

View File

@ -41,3 +41,7 @@ class Foo(models.Model):
class Bar(models.Model):
foo = models.ForeignKey(Foo, models.CASCADE, to_field='target')
m2m_foo = models.ManyToManyField(Foo, related_name='m2m_foo')
class UniqueNumber(models.Model):
number = models.IntegerField(unique=True)

View File

@ -1,9 +1,12 @@
import unittest
from django.core.exceptions import FieldError
from django.db import IntegrityError, connection, transaction
from django.db.models import Count, F, Max
from django.db.models.functions import Concat, Lower
from django.test import TestCase
from .models import A, B, Bar, D, DataPoint, Foo, RelatedPoint
from .models import A, B, Bar, D, DataPoint, Foo, RelatedPoint, UniqueNumber
class SimpleTest(TestCase):
@ -199,3 +202,38 @@ class AdvancedTests(TestCase):
with self.subTest(annotation=annotation):
with self.assertRaisesMessage(FieldError, msg):
RelatedPoint.objects.annotate(new_name=annotation).update(name=F('new_name'))
@unittest.skipUnless(
connection.vendor == 'mysql',
'UPDATE...ORDER BY syntax is supported on MySQL/MariaDB',
)
class MySQLUpdateOrderByTest(TestCase):
"""Update field with a unique constraint using an ordered queryset."""
@classmethod
def setUpTestData(cls):
UniqueNumber.objects.create(number=1)
UniqueNumber.objects.create(number=2)
def test_order_by_update_on_unique_constraint(self):
tests = [
('-number', 'id'),
(F('number').desc(), 'id'),
(F('number') * -1, 'id'),
]
for ordering in tests:
with self.subTest(ordering=ordering), transaction.atomic():
updated = UniqueNumber.objects.order_by(*ordering).update(
number=F('number') + 1,
)
self.assertEqual(updated, 2)
def test_order_by_update_on_unique_constraint_annotation(self):
# Ordering by annotations is omitted because they cannot be resolved in
# .update().
with self.assertRaises(IntegrityError):
UniqueNumber.objects.annotate(
number_inverse=F('number').desc(),
).order_by('number_inverse').update(
number=F('number') + 1,
)