#30667: Running last() on a related object with a F() ordering breaks all()
ordering (global side effect)
-------------------------------------+-------------------------------------
Reporter: | Owner: nobody
NyanKiyoshi |
Type: Bug | Status: new
Component: Database | Version: 2.2
layer (models, ORM) |
Severity: Normal | Keywords:
Triage Stage: | Has patch: 0
Unreviewed |
Needs documentation: 0 | Needs tests: 0
Patch needs improvement: 0 | Easy pickings: 0
UI/UX: 0 |
-------------------------------------+-------------------------------------
Hi,
I have ran into a weird issue when working with a O2M relation, where one
would use the meta field ordering with `F(...)`.
We can take this following code to illustrate as an example:
{{{#!python
from django.db import models
from django.db.models import F
class Item(models.Model):
pass
class ItemValue(models.Model):
sort_order = models.IntegerField(unique=True, null=True)
item = models.ForeignKey(Item, on_delete=models.CASCADE,
related_name="items")
class Meta:
ordering = (F("sort_order").asc(nulls_last=True), "id")
}}}
In `Item` we have a relation to the `items`. The most important part here,
is the ordering with a `F(...)`.
Now, if another part of the code (a view, a test, ...) runs `last()` on
the items, it will trigger some kind of switch in the ORM, which will make
it always run `F(...)` in `DESC`.
Here is an example:
{{{#!python
item = Item.objects.create()
ItemValue.objects.bulk_create([
ItemValue(item=self.item, sort_order=0),
ItemValue(item=self.item),
ItemValue(item=self.item),
])
sort_order = item.items.all()[0].sort_order # is 0
item.items.last()
sort_order = item.items.all()[0].sort_order # is None
item.items.last()
sort_order = item.items.all()[0].sort_order # is 0
}}}
The biggest issue with this, is if this is ran into a view or a test, the
ordering will always be reversed until we restart the django worker, or if
we run `item.items.last()` again to flip the switch.
Here is a full example:
{{{#!python
# models
from django.db import models
from django.db.models import F
class Item(models.Model):
pass
class ItemValue(models.Model):
sort_order = models.IntegerField(unique=True, null=True)
item = models.ForeignKey(Item, on_delete=models.CASCADE,
related_name="items")
class Meta:
ordering = (F("sort_order").asc(nulls_last=True), "id")
# test
class TestItemValue(TestCase):
def setUp(self) -> None:
self.item = Item.objects.create()
ItemValue.objects.bulk_create([
ItemValue(item=self.item, sort_order=0),
ItemValue(item=self.item),
ItemValue(item=self.item),
])
def test_valid(self):
self.assertEqual(self.item.items.all()[0].sort_order, 0)
def test_switch(self):
self.item.items.last()
with CaptureQueriesContext(connection) as ctx:
sort_order = self.item.items.all()[0].sort_order
sql = ctx[0]['sql']
order_by = sql[sql.find("ORDER BY"):]
self.assertEqual(sort_order, 0, order_by)
}}}
This is an example of the switch in action during a `.all()`:
[[Image(https://i.imgur.com/lVH1pOs.gif)]]
--
Ticket URL: <https://code.djangoproject.com/ticket/30667>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.
--
You received this message because you are subscribed to the Google Groups
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/django-updates/054.b0b3f212d1d678735402ba33dc5ed056%40djangoproject.com.