1
0
mirror of https://github.com/django/django.git synced 2025-03-05 06:44:25 +00:00

Fixed -- 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
django/db/backends/mysql
docs
ref/models
releases
tests/update

@ -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):

@ -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()``
~~~~~~~~~~~~

@ -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
~~~~~~~~~~~~~~~~~~~~~~

@ -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)

@ -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,
)