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

Lo intenté:

 Photo.objects.filter(tags__name__in=categories) 

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

Así que si las categorías fueran [‘holiday’, ‘summer’] quiero Photo’s con una etiqueta de holiday y summer.

¿Se puede lograr esto?

Related of "Django filter queryset __in para * cada * elemento en la lista"

Resumen:

Una opción es, como lo sugieren jpic y sgallen en los comentarios, agregar .filter() para cada categoría. Cada filter adicional agrega 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 breve 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]: [, ] 

Usando el enfoque de los filtros encadenados :

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

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.

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]: [] 

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 

AND objetos de ed Q 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 ) 

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']) , ]> >>> Post.objects.filter(tags__contains=['django']) , ]> >>> Post.objects.filter(tags__contains=['django', 'thoughts']) ]> 

ArrayField tiene algunas características más potentes, como la superposición y las transformaciones de índice .

Esto también puede hacerse mediante la generación dinámica de consultas 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 tu ejemplo sería igual a

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

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 tags en la lista; los operadores pueden ser cualquiera como ‘iexact’, ‘in’, ‘contiene’, ‘ne’, …

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