Encadenando múltiples filtros () en Django, ¿es esto un error?

103

Siempre asumí que encadenar múltiples llamadas filter () en Django era siempre lo mismo que recopilarlas en una sola llamada.

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

pero me he encontrado con un conjunto de consultas complicado en mi código donde este no es el 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')

El SQL generado es

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 )

El primer conjunto de consultas con las filter()llamadas encadenadas se une al modelo de Inventario dos veces, creando un OR entre las dos condiciones, mientras que el segundo conjunto de consultas aplica un AND a las dos condiciones juntas. Esperaba que la primera consulta también Y las dos condiciones. ¿Es este el comportamiento esperado o es un error en Django?

La respuesta a una pregunta relacionada ¿Hay alguna desventaja en el uso de ".filter (). Filter (). Filter () ..." en Django? parece indicar que los dos conjuntos de consultas deberían ser equivalentes.

gerdemb
fuente

Respuestas:

117

La forma en que lo entiendo es que son sutilmente diferentes por diseño (y ciertamente estoy abierto a la corrección): filter(A, B)primero se filtrará de acuerdo con A y luego se subfiltrará de acuerdo con B, mientras filter(A).filter(B)que devolverá una fila que coincide con A 'y' una potencialmente diferente fila que coincide con B.

Mira el ejemplo aquí:

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

particularmente:

Todo lo que hay dentro de una sola llamada filter () se aplica simultáneamente para filtrar los elementos que cumplen con todos esos requisitos. Las sucesivas llamadas a filter () restringen aún más el conjunto de objetos

...

En este segundo ejemplo (filter (A) .filter (B)), el primer filtro restringió el conjunto de consultas a (A). El segundo filtro restringió aún más el conjunto de blogs a aquellos que también son (B). Las entradas seleccionadas por el segundo filtro pueden ser o no las mismas que las del primer filtro.

Timmy O'Mahony
fuente
18
Este comportamiento, aunque documentado, parece violar el principio del menor asombro. Múltiples filtros () y juntos cuando los campos están en el mismo modelo, pero luego O juntos cuando abarcan relaciones.
gerdemb
3
Creo que lo tiene al revés en el primer párrafo: el filtro (A, B) es la situación AND ('lennon' Y 2008 en los documentos), mientras que el filtro (A) .filter (B) es la situación OR ( 'lennon' OR 2008). Esto tiene sentido cuando observa las consultas generadas en la pregunta: el caso .filter (A) .filter (B) crea las combinaciones dos veces, lo que da como resultado un OR.
Sam
17
filter (A, B) es el filtro AND (A) .filter (B) es OR
WeizhongTu
3
entonces further restrictsignifica less restrictive?
boh
7
Esta respuesta es incorrecta. No es "OR". Esta frase "El segundo filtro restringió el conjunto de blogs a los que también son (B)". menciona claramente "que también son (B)". Si observa un comportamiento similar a OR en este ejemplo específico, no significa necesariamente que pueda generalizar su propia interpretación. Por favor, mire las respuestas de "Kevin 3112" y "Johnny Tsang". Creo que esas son las respuestas correctas.
1man
66

Estos dos estilos de filtrado son equivalentes en la mayoría de los casos, pero cuando se consultan objetos basados ​​en ForeignKey o ManyToManyField, son ligeramente diferentes.

Ejemplos de la documentación .

El modelo
Blog to Entry es una relación de uno a varios.

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
Suponiendo que hay algunos objetos de entrada y blog aquí.
ingrese la descripción de la imagen aquí

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 la primera consulta (filtro único uno), solo coincide con blog1.

Para la segunda consulta (filtros encadenados uno), filtra blog1 y blog2.
El primer filtro restringe el conjunto de consultas a blog1, blog2 y blog5; el segundo filtro restringe el conjunto de blogs a blog1 y blog2.

Y deberías darte cuenta de que

Estamos filtrando los elementos del Blog con cada declaración de filtro, no los elementos de Entrada.

Entonces, no es lo mismo, porque Blog y Entry son relaciones de valores múltiples.

Referencia: https://docs.djangoproject.com/en/1.8/topics/db/queries/#spanning-multi-valued-relationships
Si hay algún problema, corrígeme.

Editar: Se cambió v1.6 a v1.8 ya que los enlaces 1.6 ya no están disponibles.

Kevin_wyx
fuente
3
Parece estar confundido entre "coincidencias" y "filtrar". Si se limita a "esta consulta devuelve", sería mucho más claro.
OrangeDog
7

Como puede ver en las declaraciones SQL generadas, la diferencia no es el "OR" como algunos sospechan. Así es como se coloca WHERE y JOIN.

Example1 (misma tabla unida):

(ejemplo 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)

Esto le dará todos los blogs que tienen una entrada con ambos (entry_ headline _contains = 'Lennon') Y (entry__pub_date__year = 2008), que es lo que esperaría de esta consulta. Resultado: libro con {entry.headline: 'Life of Lennon', entry.pub_date: '2008'}

Ejemplo 2 (encadenado)

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

Esto cubrirá todos los resultados del Ejemplo 1, pero generará un poco más de resultado. Porque primero filtra todos los blogs con (entry_ headline _contains = 'Lennon') y luego desde los filtros de resultados (entry__pub_date__year = 2008).

La diferencia es que también le dará resultados como: Reserve con {entry.headline: ' Lennon ', entry.pub_date: 2000}, {entry.headline: 'Bill', entry.pub_date: 2008 }

En tu caso

Creo que es este lo que necesitas:

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

Y si desea usar O, lea: https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups-with-q-objects

Johnny Tsang
fuente
El segundo ejemplo no es realmente cierto. Todos los filtros encadenados se aplican a los objetos consultados, es decir, se unen mediante AND en la consulta.
Janne
Creo que el Ejemplo 2 es correcto, y en realidad es una explicación tomada de los documentos oficiales de Django, como se hace referencia. Puede que no sea el mejor explicador y lo perdono. El ejemplo 1 es un Y directo como cabría esperar en una escritura SQL normal. El ejemplo 1 da algo como esto: 'SELECCIONAR blog ÚNETE a la entrada DONDE entry.head_line LIKE " Lennon " AND entry.year == 2008 El ejemplo 2 da algo como esto:' SELECCIONAR blog ÚNETE a la entrada DONDE entrada.head_list COMO " Lennon " UNION SELECCIONAR blog ÚNETE a la entrada DONDE entry.head_list LIKE " Lennon " '
Johnny Tsang
Señor, tiene toda la razón. De prisa, me perdí el hecho de que nuestro criterio de filtrado apunta a una relación de uno a muchos, no al blog en sí.
Janne
0

A veces no desea unir varios filtros de esta manera:

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

Y el siguiente código en realidad no devolvería lo correcto.

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

Lo que puede hacer ahora es utilizar un filtro de recuento de anotaciones.

En este caso contamos todos los turnos que pertenecen a un determinado evento.

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

Luego, puede filtrar por anotación.

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

Esta solución también es más económica en conjuntos de consultas grandes.

Espero que esto ayude.

Tobias Ernst
fuente