Django filter queryset __in para * cada * elemento en la lista

101

Digamos que tengo los siguientes modelos

class Photo(models.Model):
    tags = models.ManyToManyField(Tag)

class Tag(models.Model):
    name = models.CharField(max_length=50)

En una vista, tengo una lista con filtros activos llamados categorías . Quiero filtrar los objetos fotográficos que tienen todas las etiquetas presentes en las categorías .

Lo intenté:

Photo.objects.filter(tags__name__in=categories)

Pero esto coincide con cualquier elemento de las categorías, no con todos los elementos.

Entonces, si las categorías fueran ['vacaciones', 'verano'], quiero fotos con etiquetas de vacaciones y verano.

¿Se puede lograr esto?

Sander van Leeuwen
fuente
6
Quizás: qs = Photo.objects.all (); para categoría en categorías: qs = qs.filter (tags__name = category)
jpic
2
jpic tiene razón, Photo.objects.filter(tags__name='holiday').filter(tags__name='summer')es el camino a seguir. (Esto es lo mismo que el ejemplo de jpic). Cada uno filterdebe agregar más JOINmensajes de correo electrónico a la consulta, por lo que podría adoptar un enfoque de anotación si son demasiados.
Davor Lucic
1
Aquí está la referencia en los documentos: docs.djangoproject.com/en/dev/topics/db/queries/…
sgallen
Es de esperar que haya una función incorporada para esto por Django
Vincent

Respuestas:

124

Resumen:

Una opción es, como sugirieron jpic y sgallen en los comentarios, agregar .filter()para cada categoría. Cada adicional filteragrega más combinaciones, lo que no debería ser un problema para un pequeño conjunto de categorías.

Existe el enfoque de agregación . Esta consulta sería más corta y quizás más rápida para un gran conjunto de categorías.

También tiene la opción de utilizar consultas personalizadas .


Algunos ejemplos

Configuración de prueba:

class Photo(models.Model):
    tags = models.ManyToManyField('Tag')

class Tag(models.Model):
    name = models.CharField(max_length=50)

    def __unicode__(self):
        return self.name

In [2]: t1 = Tag.objects.create(name='holiday')
In [3]: t2 = Tag.objects.create(name='summer')
In [4]: p = Photo.objects.create()
In [5]: p.tags.add(t1)
In [6]: p.tags.add(t2)
In [7]: p.tags.all()
Out[7]: [<Tag: holiday>, <Tag: summer>]

Usando el enfoque de filtros encadenados :

In [8]: Photo.objects.filter(tags=t1).filter(tags=t2)
Out[8]: [<Photo: Photo object>]

Consulta resultante:

In [17]: print Photo.objects.filter(tags=t1).filter(tags=t2).query
SELECT "test_photo"."id"
FROM "test_photo"
INNER JOIN "test_photo_tags" ON ("test_photo"."id" = "test_photo_tags"."photo_id")
INNER JOIN "test_photo_tags" T4 ON ("test_photo"."id" = T4."photo_id")
WHERE ("test_photo_tags"."tag_id" = 3  AND T4."tag_id" = 4 )

Tenga en cuenta que cada uno filteragrega más JOINSa la consulta.

Usando el enfoque de anotación :

In [29]: from django.db.models import Count
In [30]: Photo.objects.filter(tags__in=[t1, t2]).annotate(num_tags=Count('tags')).filter(num_tags=2)
Out[30]: [<Photo: Photo object>]

Consulta resultante:

In [32]: print Photo.objects.filter(tags__in=[t1, t2]).annotate(num_tags=Count('tags')).filter(num_tags=2).query
SELECT "test_photo"."id", COUNT("test_photo_tags"."tag_id") AS "num_tags"
FROM "test_photo"
LEFT OUTER JOIN "test_photo_tags" ON ("test_photo"."id" = "test_photo_tags"."photo_id")
WHERE ("test_photo_tags"."tag_id" IN (3, 4))
GROUP BY "test_photo"."id", "test_photo"."id"
HAVING COUNT("test_photo_tags"."tag_id") = 2

ANDLos Qobjetos ed no funcionarían:

In [9]: from django.db.models import Q
In [10]: Photo.objects.filter(Q(tags__name='holiday') & Q(tags__name='summer'))
Out[10]: []
In [11]: from operator import and_
In [12]: Photo.objects.filter(reduce(and_, [Q(tags__name='holiday'), Q(tags__name='summer')]))
Out[12]: []

Consulta resultante:

In [25]: print Photo.objects.filter(Q(tags__name='holiday') & Q(tags__name='summer')).query
SELECT "test_photo"."id"
FROM "test_photo"
INNER JOIN "test_photo_tags" ON ("test_photo"."id" = "test_photo_tags"."photo_id")
INNER JOIN "test_tag" ON ("test_photo_tags"."tag_id" = "test_tag"."id")
WHERE ("test_tag"."name" = holiday  AND "test_tag"."name" = summer )
Davor Lucic
fuente
6
¿Existe una solución con una búsqueda personalizada? docs.djangoproject.com/en/1.10/howto/custom-lookups Sería genial cambiar "__in" a "__all" y hacer que cree la consulta sql correcta.
t1m0
1
Esta solución de anotación parece incorrecta. ¿Qué pasa si hay tres etiquetas posibles (llamemos a la adicional para t3, y una foto tiene las etiquetas t2y t3. Entonces, esta foto seguirá coincidiendo con la consulta dada)
beruic
@beruic Creo que la idea es reemplazar num_tags = 2 con num_tags = len (etiquetas); Supongo que el 2 codificado de forma rígida fue solo por ejemplo.
tbm
3
@tbm Todavía no funcionaría. Photo.objects.filter(tags__in=tags)coincide con las fotos que tienen alguna de las etiquetas, no solo las que las tienen todas. Algunos de los que solo tienen una de las etiquetas deseadas, pueden tener exactamente la cantidad de etiquetas que está buscando, y algunos de los que tienen todas las etiquetas deseadas, también pueden tener etiquetas adicionales.
beruic
1
@beruic la anotación cuenta solo las etiquetas devueltas por la consulta, por lo que si (num etiquetas devueltas por la consulta) == (num etiquetas buscadas) entonces se incluye la fila; Las etiquetas "extra" no se buscan, por lo que no se contarán. Lo he verificado en mi propia aplicación.
tbm
8

Otro enfoque que funciona, aunque solo PostgreSQL, es usar django.contrib.postgres.fields.ArrayField:

Ejemplo copiado de documentos :

>>> Post.objects.create(name='First post', tags=['thoughts', 'django'])
>>> Post.objects.create(name='Second post', tags=['thoughts'])
>>> Post.objects.create(name='Third post', tags=['tutorial', 'django'])

>>> Post.objects.filter(tags__contains=['thoughts'])
<QuerySet [<Post: First post>, <Post: Second post>]>

>>> Post.objects.filter(tags__contains=['django'])
<QuerySet [<Post: First post>, <Post: Third post>]>

>>> Post.objects.filter(tags__contains=['django', 'thoughts'])
<QuerySet [<Post: First post>]>

ArrayFieldtiene algunas características más poderosas, como la superposición y las transformaciones de índice .

Sander van Leeuwen
fuente
3

Esto también se puede hacer mediante la generación de consultas dinámicas usando Django ORM y algo de magia de Python :)

from operator import and_
from django.db.models import Q

categories = ['holiday', 'summer']
res = Photo.filter(reduce(and_, [Q(tags__name=c) for c in categories]))

La idea es generar objetos Q apropiados para cada categoría y luego combinarlos usando el operador AND en un QuerySet. Por ejemplo, para su ejemplo sería igual a

res = Photo.filter(Q(tags__name='holiday') & Q(tags__name='summer'))
demalexx
fuente
3
Esto no funcionaría. Sus ejemplos de consulta no devolverían nada para los modelos en cuestión.
Davor Lucic
Gracias por la corrección. Pensé que encadenar filtersería lo mismo que usar andpara objetos Q en un filtro ... Mi error.
demalexx
No te preocupes, mi primer pensamiento fue también Q objetos.
Davor Lucic
1
Esto sería más lento si trabaja con tablas grandes y datos grandes para comparar. (como 1 millón cada uno)
gies0r
Este enfoque debería funcionar si se cambia de filtera excludey utilizar un operador cambiar signo. Así: res = Photo.exclude(~reduce(and_, [Q(tags__name=c) for c in categories]))
Ben
1

Utilizo una pequeña función que itera filtros sobre una lista para un operador dado y un nombre de columna:

def exclusive_in (cls,column,operator,value_list):         
    myfilter = column + '__' + operator
    query = cls.objects
    for value in value_list:
        query=query.filter(**{myfilter:value})
    return query  

y esta función se puede llamar así:

exclusive_in(Photo,'tags__name','iexact',['holiday','summer'])

también funciona con cualquier clase y más etiquetas en la lista; los operadores pueden ser cualquiera como 'iexact', 'in', 'contains', 'ne', ...

David
fuente
0
queryset = Photo.objects.filter(tags__name="vacaciones") | Photo.objects.filter(tags__name="verano")
Edgar Eduardo de los santos
fuente
-1

Si queremos hacerlo de forma dinámica, seguimos el ejemplo:

tag_ids = [t1.id, t2.id]
qs = Photo.objects.all()

for tag_id in tag_ids:
    qs = qs.filter(tag__id=tag_id)    

print qs
tarasinf
fuente
No puede funcionar tan pronto como la segunda iteración, el conjunto de consultas estará vacío
lapin