Encadeando filtro múltiplo () no Django, isso é um bug?

103

Sempre presumi que encadear várias chamadas de filter () no Django era sempre o mesmo que coletá-las em uma única chamada.

# Equivalent
Model.objects.filter(foo=1).filter(bar=2)
Model.objects.filter(foo=1,bar=2)

mas eu encontrei um queryset complicado em meu código onde este não é o caso

class Inventory(models.Model):
    book = models.ForeignKey(Book)

class Profile(models.Model):
    user = models.OneToOneField(auth.models.User)
    vacation = models.BooleanField()
    country = models.CharField(max_length=30)

# Not Equivalent!
Book.objects.filter(inventory__user__profile__vacation=False).filter(inventory__user__profile__country='BR')
Book.objects.filter(inventory__user__profile__vacation=False, inventory__user__profile__country='BR')

O SQL gerado é

SELECT "library_book"."id", "library_book"."asin", "library_book"."added", "library_book"."updated" FROM "library_book" INNER JOIN "library_inventory" ON ("library_book"."id" = "library_inventory"."book_id") INNER JOIN "auth_user" ON ("library_inventory"."user_id" = "auth_user"."id") INNER JOIN "library_profile" ON ("auth_user"."id" = "library_profile"."user_id") INNER JOIN "library_inventory" T5 ON ("library_book"."id" = T5."book_id") INNER JOIN "auth_user" T6 ON (T5."user_id" = T6."id") INNER JOIN "library_profile" T7 ON (T6."id" = T7."user_id") WHERE ("library_profile"."vacation" = False  AND T7."country" = BR )
SELECT "library_book"."id", "library_book"."asin", "library_book"."added", "library_book"."updated" FROM "library_book" INNER JOIN "library_inventory" ON ("library_book"."id" = "library_inventory"."book_id") INNER JOIN "auth_user" ON ("library_inventory"."user_id" = "auth_user"."id") INNER JOIN "library_profile" ON ("auth_user"."id" = "library_profile"."user_id") WHERE ("library_profile"."vacation" = False  AND "library_profile"."country" = BR )

O primeiro queryset com as filter()chamadas encadeadas junta-se ao modelo Inventory duas vezes, criando efetivamente um OR entre as duas condições, enquanto o segundo queryset ANDs as duas condições juntas. Eu esperava que a primeira consulta também executasse AND nas duas condições. Este é o comportamento esperado ou é um bug no Django?

A resposta a uma pergunta relacionada Existe uma desvantagem em usar ".filter (). Filter (). Filter () ..." no Django? parece indicar que os dois querysets devem ser equivalentes.

gerdemb
fonte

Respostas:

117

A forma como eu entendo é que eles são sutilmente diferentes por design (e eu certamente estou aberto para correção): filter(A, B)primeiro filtrarei de acordo com A e depois subfiltre de acordo com B, enquantofilter(A).filter(B) retornará uma linha que corresponde a A 'e' um potencialmente diferente linha que corresponde a B.

Veja o exemplo aqui:

https://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships

particularmente:

Tudo dentro de uma única chamada filter () é aplicado simultaneamente para filtrar os itens que correspondem a todos esses requisitos. Chamadas de filtro sucessivas () restringem ainda mais o conjunto de objetos

...

Neste segundo exemplo (filter (A) .filter (B)), o primeiro filtro restringiu o queryset a (A). O segundo filtro restringiu o conjunto de blogs ainda mais àqueles que também são (B). As entradas selecionadas pelo segundo filtro podem ou não ser iguais às entradas do primeiro filtro.

Timmy O'Mahony
fonte
18
Esse comportamento, embora documentado, parece violar o princípio do menor espanto. E vários filtros () estão juntos quando os campos estão no mesmo modelo, mas depois OU juntos quando abrangem relacionamentos.
gerdemb
3
Acredito que você entendeu errado no primeiro parágrafo - o filtro (A, B) é a situação AND ('lennon' E 2008 nos documentos), enquanto o filtro (A) .filter (B) é a situação OR ( 'lennon' OU 2008). Isso faz sentido quando você olha para as consultas geradas na pergunta - o caso .filter (A) .filter (B) cria as junções duas vezes, resultando em um OR.
Sam
17
filtro (A, B) é o filtro AND (A). filtro (B) é OR
WeizhongTu
3
isso further restrictsignifica less restrictive?
boh
7
Esta resposta está incorreta. Não é "OU". Esta frase "O segundo filtro restringiu o conjunto de blogs ainda mais àqueles que também são (B)." menciona claramente "que também são (B)." Se você observar um comportamento semelhante a OR neste exemplo específico, isso não significa necessariamente que você pode generalizar sua própria interpretação. Veja as respostas de "Kevin 3112" e "Johnny Tsang". Eu acredito que essas são as respostas corretas.
1man
66

Esses dois estilos de filtragem são equivalentes na maioria dos casos, mas quando consulta em objetos baseados em ForeignKey ou ManyToManyField, eles são ligeiramente diferentes.

Exemplos da documentação .

O modelo
Blog para entrada é uma relação um-para-muitos.

from django.db import models

class Blog(models.Model):
    ...

class Entry(models.Model):
    blog = models.ForeignKey(Blog)
    headline = models.CharField(max_length=255)
    pub_date = models.DateField()
    ...

objetos
Supondo que haja alguns objetos de blog e de entrada aqui.
insira a descrição da imagem aqui

consultas

Blog.objects.filter(entry__headline_contains='Lennon', 
    entry__pub_date__year=2008)
Blog.objects.filter(entry__headline_contains='Lennon').filter(
    entry__pub_date__year=2008)  

Para a primeira consulta (filtro único um), corresponde apenas a blog1.

Para a segunda consulta (filtros encadeados um), ele filtra blog1 e blog2.
O primeiro filtro restringe o queryset a blog1, blog2 e blog5; o segundo filtro restringe o conjunto de blogs ainda mais para blog1 e blog2.

E você deve perceber que

Estamos filtrando os itens do Blog com cada instrução de filtro, não os itens de entrada.

Portanto, não é a mesma coisa, porque Blog e Entry são relacionamentos de vários valores.

Referência: https://docs.djangoproject.com/en/1.8/topics/db/queries/#spanning-multi-valued-relationships
Se houver algo errado, por favor me corrija.

Editar: v1.6 alterado para v1.8 uma vez que os links 1.6 não estão mais disponíveis.

Kevin_wyx
fonte
3
Você parece estar confuso entre "correspondências" e "filtros para fora". Se você ficasse com "esta consulta retorna", seria muito mais claro.
OrangeDog
7

Como você pode ver nas instruções SQL geradas, a diferença não é o "OU", como alguns podem suspeitar. É como WHERE e JOIN são colocados.

Exemplo1 (mesma tabela associada):

(exemplo de https://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships )

Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008)

Isso lhe dará todos os Blogs que têm uma entrada com ambos (entrada_ título _contains = 'Lennon') E (entry__pub_date__year = 2008), que é o que você esperaria desta consulta. Resultado: Reserve com {entry.headline: 'Life of Lennon', entry.pub_date: '2008'}

Exemplo 2 (encadeado)

Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)

Isso cobrirá todos os resultados do Exemplo 1, mas irá gerar um pouco mais de resultados. Porque primeiro filtra todos os blogs com (entry_ headline _contains = 'Lennon') e depois dos filtros de resultado (entry__pub_date__year = 2008).

A diferença é que ele também fornecerá resultados como: Reserve com {entry.headline: ' Lennon ', entry.pub_date: 2000}, {entry.headline: 'Bill', entry.pub_date: 2008 }

No seu caso

Acho que é este que você precisa:

Book.objects.filter(inventory__user__profile__vacation=False, inventory__user__profile__country='BR')

E se você deseja usar OR, leia: https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups-with-q-objects

Johnny Tsang
fonte
O segundo exemplo não é realmente verdadeiro. Todos os filtros encadeados são aplicados aos objetos consultados, ou seja, são colocados em AND na consulta.
Janne de
Eu acredito que o Exemplo 2 está correto, e na verdade é uma explicação tirada dos documentos oficiais do Django, conforme referenciado. Posso não ser o melhor explicador e me desculpo por isso. O exemplo 1 é um AND direto, como você esperaria em uma escrita SQL normal. O exemplo 1 mostra algo como: 'SELECT blog JOIN entry WHERE entry.head_line LIKE " Lennon " AND entry.year == 2008 O exemplo 2 fornece algo como:' SELECT blog JOIN entry WHERE entry.head_list LIKE " Lennon " UNION SELECT blog PARTICIPAR da entrada WHERE entry.head_list LIKE " Lennon " '
Johnny Tsang
Senhor, você está certo. Com pressa, perdi o fato de que nossos critérios de filtragem estão apontando para uma relação um-para-muitos, não para o blog em si.
Janne
0

Às vezes, você não deseja unir vários filtros como este:

def your_dynamic_query_generator(self, event: Event):
    qs \
    .filter(shiftregistrations__event=event) \
    .filter(shiftregistrations__shifts=False)

E o código a seguir realmente não retornaria a coisa correta.

def your_dynamic_query_generator(self, event: Event):
    return Q(shiftregistrations__event=event) & Q(shiftregistrations__shifts=False)

O que você pode fazer agora é usar um filtro de contagem de anotação.

Neste caso, contamos todos os turnos que pertencem a um determinado evento.

qs: EventQuerySet = qs.annotate(
    num_shifts=Count('shiftregistrations__shifts', filter=Q(shiftregistrations__event=event))
)

Depois, você pode filtrar por anotação.

def your_dynamic_query_generator(self):
    return Q(num_shifts=0)

Esta solução também é mais barata em grandes querysets.

Espero que isto ajude.

Tobias Ernst
fonte