Disons que j'ai les modèles suivants
class Photo(models.Model):
tags = models.ManyToManyField(Tag)
class Tag(models.Model):
name = models.CharField(max_length=50)
Dans une vue, j'ai une liste de filtres actifs appelée catégories. Je souhaite filtrer les objets Photo contenant toutes les balises présentes dans categories.
J'ai essayé:
Photo.objects.filter(tags__name__in=categories)
Mais cela correspond à tout élément dans les catégories, pas tous éléments.
Donc, si les catégories sont ['vacances', 'été'], je veux des photos avec une étiquette vacances et une étiquette estivale.
Cela peut-il être réalisé?
Résumé:
Une option consiste, comme suggéré par jpic et sgallen dans les commentaires, à ajouter .filter()
pour chaque catégorie. Chaque filter
supplémentaire ajoute plus de jointures, ce qui ne devrait pas poser de problème pour un petit ensemble de catégories.
Il y a le agrégationapproche . Cette requête serait plus courte et peut-être plus rapide pour un grand ensemble de catégories.
Vous avez également la possibilité d'utiliser requêtes personnalisées .
Quelques exemples
Configuration de test:
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>]
In [8]: Photo.objects.filter(tags=t1).filter(tags=t2)
Out[8]: [<Photo: Photo object>]
Requête résultante:
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 )
Notez que chaque filter
ajoute plus de JOINS
à la requête.
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>]
Requête résultante:
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
ed Q
ne fonctionneraient pas: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]: []
Requête résultante:
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 )
Une autre approche qui fonctionne, bien que PostgreSQL uniquement, utilise Django.contrib.postgres.fields.ArrayField
:
Exemple copié de docs :
>>> 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
possède des fonctionnalités plus puissantes telles que chevauchement et transformations d'index .
Cela peut aussi être fait par la génération de requêtes dynamiques en utilisant Django ORM et certains Python magie :)
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]))
L'idée est de générer des objets Q appropriés pour chaque catégorie, puis de les combiner à l'aide de l'opérateur AND dans un ensemble de requêtes. Par exemple. pour votre exemple, il serait égal à
res = Photo.filter(Q(tags__name='holiday') & Q(tags__name='summer'))
J'utilise une petite fonction qui itère les filtres sur une liste pour un opérateur donné et un nom de colonne:
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
et cette fonction peut s'appeler comme ça:
exclusive_in(Photo,'tags__name','iexact',['holiday','summer'])
cela fonctionne aussi avec n'importe quelle classe et plus de balises dans la liste; les opérateurs peuvent être n'importe qui comme "iexact", "dans", "contient", "ne", ...