Понимание order_by многозначных полей (Django)

#django #many-to-many #manytomanyfield

#django #многие ко многим #manytomanyfield

Вопрос:

Прочитав документы django order_by , есть примечание / предупреждение, в котором (если я правильно понял) говорится, что:

  • Если вы упорядочиваете набор запросов с использованием многозначного поля, то каждый элемент в этом наборе запросов, который имеет несколько связанных элементов, будет добавлен несколько раз в результирующий набор запросов, созданный order_by .

Я попытался протестировать это на базовом примере:

Минимальный воспроизводимый пример

 class Pizza(models.Model):
    name = models.CharField(max_length=100)
    toppings = models.ManyToManyField('Topping', through='PizzaToppings')

class PizzaToppings(models.Model):
    pizza = models.ForeignKey('Pizza', on_delete=models.CASCADE, related_name="pizza_toppings")
    topping = models.ForeignKey('Topping', on_delete=models.CASCADE, related_name="pizzas_with_topping")
    amount = models.IntegerField()

    class Meta:
        ordering = ["amount",]

class Topping(models.Model):
    ingredient = models.CharField(max_length=100)
 

затем

 >>> p1 = Pizza.objects.create(name="Cheese and Tomato")
>>> p2 = Pizza.objects.create(name="Pepperoni")
>>> cheese = Topping.objects.create(ingredient="Cheese")
>>> tomato = Topping.objects.create(ingredient="Tomato puree")
>>> p1.toppings.add(cheese, through_defaults={"amount":4})
>>> p1.toppings.add(tomato, through_defaults={"amount":3})
>>> p2.toppings.add(cheese, through_defaults={"amount":2})
>>> p2.toppings.add(tomato, through_defaults={"amount":1})
 

Пока все нормально. Но здесь все становится запутанным:

 >>> q1 = Topping.objects.all()
<QuerySet [<Topping: Topping object (1)>, <Topping: Topping object (2)>]>
>>> q2 = p1.toppings.all()
<QuerySet [<Topping: Topping object (1)>, <Topping: Topping object (2)>]>
>>> q1.order_by("pizzas_with_topping")
<QuerySet [<Topping: Topping object (2)>, <Topping: Topping object (1)>, <Topping: Topping object (2)>, <Topping: Topping object (1)>]>
>>> q2.order_by("pizzas_with_topping")
<QuerySet [<Topping: Topping object (2)>, <Topping: Topping object (1)>]>
 

Проблема

Как видно выше, наборы запросов идентичны с точки зрения элементов, которые они содержат. Но когда один q1 упорядочен, мы получаем поведение, описанное в документации. В q2 мы не получаем такого поведения. Предположительно, это связано с тем, что django делает что-то умное, поскольку набор запросов связан с начинками, связанными с p1 .

Вопрос (ы)

Что на самом деле происходит «под капотом» для обеспечения такого поведения? Наборы запросов одинаковы (если я правильно понял), так почему же order_by они ведут себя по-разному для двух наборов запросов.

Комментарии:

1. Если вы заказываете by , вы создаете JOIN , for q1 , это, таким образом, присоединится к on PizzaToppings . Для последнего набор запросов уже объединен из-за фильтрации, и, таким образом, вы исключаете определенные строки в СОЕДИНЕНИИ.


Ответ №1:

Два набора запросов различны. Первый набор запросов представляет запрос типа:

 -- q1
SELECT *
FROM topping
 

Запрос, который представлен, q2 выглядит следующим образом:

 -- q2
SELECT *
FROM topping
INNER JOIN pizzatoppings ON pizzatoppings.topping_id = topping.id
WHERE pizzatoppings.pizza_id = id-of-pizza 

Если вы затем выполните a .order_by('pizzas_with_topping') , то вы, таким образом, создаете a JOIN в таблице PizzaToppings модели и, таким образом, упорядочиваете по первичному ключу этой таблицы. Таким образом, для первого набора запросов это выглядит следующим образом:

 -- q1.order_by('pizzas_with_topping')
SELECT *
FROM topping
LEFT OUTER JOIN pizzatoppings ON pizzatoppings.topping_id = topping.id
ORDER BY pizzatoppings.id 

для последнего вы фильтруете соединение, которое уже существует:

 -- q2.order_by('pizzas_with_topping')
SELECT *
FROM topping
INNER JOIN pizzatoppings ON pizzatoppings.topping_id = topping.id
WHERE pizzatoppings.pizza_id = id-of-pizza
ORDER BY pizzatoppings.id 

таким образом, это означает, что если одна и та же начинка используется для нескольких пицц, for q1 , она будет появляться каждый раз для каждой пиццы, тогда как for q2 мы уже отфильтровали для пиццы и, таким образом, извлекаем каждую Topping для этой пиццы, а не для других.

Комментарии:

1. Большое вам спасибо за очень подробное объяснение 🙂 Имеет смысл