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


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 objetos de fotos que tienen todas las etiquetas presentes en categorías.

Lo intenté:

Photo.objects.filter(tags__name__in=categories)

Pero esto coincide alguna elemento en categorías, no todas artículos.

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

¿Se puede lograr esto?


75
2017-12-23 16:01


origen


Respuestas:


Resumen:

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

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

También tienes la opción de usar 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>]

Utilizando filtros encadenados enfoque:

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 filter agrega más JOINS a la consulta.

Utilizando anotación  enfoque:

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

ANDed Q los objetos 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 )

99
2017-12-26 17:54



Otro enfoque que funciona, aunque solo PostgreSQL, está usando 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>]>

ArrayField tiene algunas características más potentes, como superposición y índice transforma.


5
2018-01-11 13:34



Esto también se puede hacer mediante generación dinámica de consultas utilizando 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. P.ej. para su ejemplo, sería igual a

res = Photo.filter(Q(tags__name='holiday') & Q(tags__name='summer'))

2
2017-12-26 15:00



Si queremos hacerlo dinámicamente, 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

-1
2018-05-05 08:16