#35751: Ordering a model via a m2m field creates unintended side effect for
ForeignKeys
-------------------------------------+-------------------------------------
     Reporter:  Dennis Scheiba       |                    Owner:  (none)
         Type:  Bug                  |                   Status:  new
    Component:  Database layer       |                  Version:  5.1
  (models, ORM)                      |
     Severity:  Normal               |               Resolution:
     Keywords:  ORM ordering         |             Triage Stage:
                                     |  Unreviewed
    Has patch:  0                    |      Needs documentation:  0
  Needs tests:  0                    |  Patch needs improvement:  0
Easy pickings:  0                    |                    UI/UX:  0
-------------------------------------+-------------------------------------
Description changed by Dennis Scheiba:

Old description:

> Python 3.10 @ Django 5.1.1
>
> Using a many to many relation for ordering (which is something you
> shouldn't do?) will affect the traversing when the object in question is
> accessed via a foreign key - the necessary left join for the ordering
> "spills" into the ORM results, yielding n (number of foreign key
> references) times m (number of many-to-many relations within the object)
> objects instead of just the actual n objects referencing the foreign key.
>
> If you comment out the ordering the ORM behaves as expected.
>
> Th ORM results should not "multiply" due to an ordering configuration.
>
> == How to reproduce
>
> Given a toy `models.py` on a new project which looks like
>
> {{{#!python
> from django.db import models
>
> class Order(models.Model):
>     pass
>
> class BookingTime(models.Model):
>     date = models.DateTimeField(auto_now=True)
>

> class OrderItem(models.Model):
>     order = models.ForeignKey(
>         Order,
>         on_delete=models.CASCADE,
>         related_name="order_items",
>     )
>
>     booking_times = models.ManyToManyField(
>         "BookingTime",
>         related_name="order_items",
>     )
>
>     class Meta:
>         ordering = [
>             # this is the problem!
>             'booking_times__date',
>         ]
>
> }}}
>
> Then on a shell do
>
> {{{#!python
> In [1]: from foo.models import *
>
> In [2]: booking_times = [BookingTime() for _ in range(4)]
>
> In [3]: [b.save() for b in booking_times]
> Out[3]: [None, None, None, None]
>
> In [4]: order = Order()
>
> In [5]: order.save()
>
> In [6]: order_item = OrderItem(order=order)
>
> In [7]: order_item.save()
>
> In [8]: order_item.booking_times.add(*booking_times)
>
> In [9]: order.order_items.count()
> Out[9]: 1
>
> In [10]: order.order_items.all()
> Out[10]: <QuerySet [<OrderItem: OrderItem object (1)>, <OrderItem:
> OrderItem object (1)>, <OrderItem: OrderItem object (1)>, <OrderItem:
> OrderItem object (1)>]>
>
> In [11]: print(order.order_items.all().query)
> SELECT "foo_orderitem"."id", "foo_orderitem"."order_id" FROM
> "foo_orderitem" LEFT OUTER JOIN "foo_orderitem_booking_times" ON
> ("foo_orderitem"."id" = "foo_orderitem_booking_times"."orderitem_id")
> LEFT OUTER JOIN "foo_bookingtime" ON
> ("foo_orderitem_booking_times"."bookingtime_id" = "foo_bookingtime"."id")
> WHERE "foo_orderitem"."order_id" = 2 ORDER BY "foo_bookingtime"."date"
> ASC
>
> In [12]: order.order_items.all().explain()
> Out[12]: '5 0 0 SEARCH foo_orderitem USING COVERING INDEX
> foo_orderitem_order_id_e19c2dbd (order_id=?)\n11 0 0 SEARCH
> foo_orderitem_booking_times USING COVERING INDEX
> foo_orderitem_booking_times_orderitem_id_bookingtime_id_49667055_uniq
> (orderitem_id=?) LEFT-JOIN\n17 0 0 SEARCH foo_bookingtime USING INTEGER
> PRIMARY KEY (rowid=?) LEFT-JOIN\n35 0 0 USE TEMP B-TREE FOR ORDER BY'
> }}}
>
> This also happens in a template, e.g.
>
> {{{#!python
> {% for item in order.order_items.all %}
>   {{ item }}
> {% endfor %}
> }}}
>
> also yields 4 items instead of 1 due to the left join.

New description:

 Python 3.10 @ Django 5.1.1

 Using a many to many relation for ordering (which is something you
 shouldn't do?) will affect the traversing when the object in question is
 accessed via a foreign key - the necessary left join for the ordering
 "spills" into the ORM results, yielding n (number of foreign key
 references) times m (number of many-to-many relations within the object)
 objects instead of just the actual n objects referencing the foreign key.

 If you comment out the ordering the ORM behaves as expected.

 Th ORM results should not "multiply" due to an ordering configuration.

 == How to reproduce

 Given a toy `models.py` on a new project which looks like

 {{{#!python
 from django.db import models

 class Order(models.Model):
     pass

 class BookingTime(models.Model):
     date = models.DateTimeField(auto_now=True)


 class OrderItem(models.Model):
     order = models.ForeignKey(
         Order,
         on_delete=models.CASCADE,
         related_name="order_items",
     )

     booking_times = models.ManyToManyField(
         "BookingTime",
         related_name="order_items",
     )

     class Meta:
         ordering = [
             # this is the problem!
             'booking_times__date',
         ]

 }}}

 Then on a shell do

 {{{#!python
 In [1]: from foo.models import *

 In [2]: booking_times = [BookingTime() for _ in range(4)]

 In [3]: [b.save() for b in booking_times]
 Out[3]: [None, None, None, None]

 In [4]: order = Order()

 In [5]: order.save()

 In [6]: order_item = OrderItem(order=order)

 In [7]: order_item.save()

 In [8]: order_item.booking_times.add(*booking_times)

 In [9]: order.order_items.count()
 Out[9]: 1

 In [10]: order.order_items.all()
 Out[10]: <QuerySet [<OrderItem: OrderItem object (1)>, <OrderItem:
 OrderItem object (1)>, <OrderItem: OrderItem object (1)>, <OrderItem:
 OrderItem object (1)>]>

 In [11]: print(order.order_items.all().query)
 SELECT "foo_orderitem"."id", "foo_orderitem"."order_id" FROM
 "foo_orderitem" LEFT OUTER JOIN "foo_orderitem_booking_times" ON
 ("foo_orderitem"."id" = "foo_orderitem_booking_times"."orderitem_id") LEFT
 OUTER JOIN "foo_bookingtime" ON
 ("foo_orderitem_booking_times"."bookingtime_id" = "foo_bookingtime"."id")
 WHERE "foo_orderitem"."order_id" = 2 ORDER BY "foo_bookingtime"."date" ASC

 In [12]: order.order_items.all().explain()
 Out[12]: '5 0 0 SEARCH foo_orderitem USING COVERING INDEX
 foo_orderitem_order_id_e19c2dbd (order_id=?)\n11 0 0 SEARCH
 foo_orderitem_booking_times USING COVERING INDEX
 foo_orderitem_booking_times_orderitem_id_bookingtime_id_49667055_uniq
 (orderitem_id=?) LEFT-JOIN\n17 0 0 SEARCH foo_bookingtime USING INTEGER
 PRIMARY KEY (rowid=?) LEFT-JOIN\n35 0 0 USE TEMP B-TREE FOR ORDER BY'
 }}}

 This also happens in a template, e.g.

 {{{#!python
 {% for item in order.order_items.all %}
   {{ item }}
 {% endfor %}
 }}}

 also yields 4 times the same item instead of just once (due to the left
 join?).

--
-- 
Ticket URL: <https://code.djangoproject.com/ticket/35751#comment:3>
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/01070191dd3b0137-2218f3cb-3c12-46fe-96e9-1fecd7045db8-000000%40eu-central-1.amazonses.com.

Reply via email to