2015-12-26 5 views
4

У меня есть следующие 2 модели Django:Почему Django QuerySet с выражением Q() возвращает повторяющиеся значения?

from mptt.models import MPTTModel, TreeForeignKey 
from django.db import models 
from django.db.models import Q 

class Model1(MPTTModel): 
    random_field = models.IntegerField() 
    parent = TreeForeignKey('self', null=True, blank=True) 


class Model2(models.Model): 
    model_1 = models.ManyToManyField(Model1) 

    @staticmethod 
    def descendants_queryset(model1): 
     q = Q() 
     for curr_descendant in model1.get_descendants: 
      q |= Q(model_1=curr_descendant) 
     return q 

я создал экземпляры, как это:

>>> a = Model2.objects.create() 
>>> b = Model1.objects.create(random_field=1, parent=None) 
>>> c = Model1.objects.create(random_field=2, parent=b) 
>>> d = Model1.objects.create(random_field=3, parent=b) 
>>> a.model_1.add(c) 
>>> a.pk 
3 

Когда я нормальный QuerySet фильтр и, когда я использую Q() выражение оно производит Такие же результаты (как и ожидалось):

>>> [i.pk for i in Model2.objects.filter(pk=3)] 
[3] 
>>> [i.pk for i in Model2.objects.filter(Model2.descendants_queryset(b), pk=3)] 
[3] 

Но когда я добавить еще один экземпляр Model1 к ManyToMany отношений, я вижу странное дублирование только тогда, когда Я фильтрую с использованием выражения Q():

>>> a.model_1.add(d) 
>>> [i.pk for i in Model2.objects.filter(pk=3)] 
[3] 
>>> [i.pk for i in Model2.objects.filter(Model2.descendants_queryset(b), pk=3)] 
[3, 3] 

Я смущен, почему это дублирование происходит. Мне кажется, что это ошибка. Я, очевидно, обойдусь, добавив .distinct() в запрос. Но похоже, что это не обязательно. Почему это происходит и что является правильным решением?

ответ

4

я заметил, когда вы добавляете третий элемент к, ваш вывод не только дублированы, но в три раза:

>>> 4 = Model1.objects.create(random_field=3, parent=b) 
>>> a.model_1.add(e) 
>>> [i.pk for i in Model2.objects.filter(Model2.descendants_queryset(b), pk=3)] 
[3, 3, 3] 

И в четыре раза, если добавить еще один и так далее ...

Так что я Догадка заключается в том, что, поскольку ваш запрос Q() в descendants_queryset() имеет ORed, он возвращает каждый объект, у которого есть объект b как родительский, и фильтр имеет несколько совпадений для (с множеством ссылок на объекты Model1).

Если мы посмотрим на сырье SQL для Model2.objects.filter(Model2.descendants_queryset(b)), мы видим следующее:

>>> Model2.objects.filter(Model2.descendants_queryset(b)).query.sql_with_params() 
(u'SELECT "Foo_model2"."id" FROM "Foo_model2" LEFT OUTER JOIN "Foo_model2_model_1" ON ("Foo_model2"."id" = "Foo_model2_model_1"."model2_id") WHERE ("Foo_model2_model_1"."model1_id" = %s OR "Foo_model2_model_1"."model1_id" = %s OR "Foo_model2_model_1"."model1_id" = %s)', (17, 18, 19)) 

Или более читабельным:

SELECT "Foo_model2"."id" 
FROM "Foo_model2" 
    LEFT OUTER JOIN "Foo_model2_model_1" 
    ON ("Foo_model2"."id" = "Foo_model2_model_1"."model2_id") 
WHERE ("Foo_model2_model_1"."model1_id" = 17 
    OR "Foo_model2_model_1"."model1_id" = 18 
    OR "Foo_model2_model_1"."model1_id" = 19) 

Так что на самом деле конкатенации запросы, которые генерируются q |= Q(model_1=curr_descendant) с OR, который возвращает не один, а в этом случае три ссылки (все на тот же объект Model2, который содержит ManyToMany-ссылки на три объекта Model1). Это связано с заявлением о соединении - см. here для некоторых примеров.

Если мы добавим дополнительный фильтр для pk=3, он не ограничивает вывод, поскольку PK для всех возвращенных объектов идентичен (3).

Если добавить еще один объект model2 и добавьте с в качестве ссылки на новые элементы Model1 ManyToMany ссылки, то получим следующее:

>>> a2 = Model2.objects.create() 
>>> a2.model_1.add(c) 
>>> [i.pk for i in Model2.objects.filter(Model2.descendants_queryset(b))] 
[3, 3, 3, 4] 

Идентификатор нового объекта model2 проявляется в QuerySet как ну, так как он также имеет одну ссылку на объект model1.

У меня нет никаких размытых идей для решения наилучшего решения прямо сейчас, но вызов .distinct() по набору запросов кажется мне прямолинейным.