Django filter queryset __in pour * chaque * élément de la liste

102

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 avec des filtres actifs appelés catégories . Je souhaite filtrer les objets photo dont toutes les balises sont présentes dans des catégories .

J'ai essayé:

Photo.objects.filter(tags__name__in=categories)

Mais cela correspond à n'importe quel élément des catégories, pas à tous les éléments.

Donc, si les catégories sont ['vacances', 'été'], je veux des photos avec à la fois une balise vacances et été.

Cela peut-il être réalisé?

Sander van Leeuwen
la source
7
Peut-être: qs = Photo.objects.all (); pour la catégorie dans les catégories: qs = qs.filter (tags__name = category)
jpic
2
jpic a raison, Photo.objects.filter(tags__name='holiday').filter(tags__name='summer')c'est la voie à suivre. (C'est le même que l'exemple de jpic). Chacun filterdevrait ajouter plus de JOINs à la requête, vous pouvez donc adopter une approche d'annotation s'ils sont trop nombreux.
Davor Lucic
1
Voici la référence dans la documentation: docs.djangoproject.com/en/dev/topics/db/queries/…
sgallen
Vous vous attendriez à ce qu'il y ait une fonction intégrée pour cela par Django
Vincent

Réponses:

124

Résumé:

Une option est, comme suggéré par jpic et sgallen dans les commentaires, d'ajouter .filter()pour chaque catégorie. Chaque ajout filterajoute plus de jointures, ce qui ne devrait pas poser de problème pour un petit ensemble de catégories.

Il y a l' agrégation approche d' . 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 des requêtes personnalisées .


Quelques exemples

Configuration du 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>]

Utilisation de filtres chaînés approche des :

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 JOINSà la requête.

Utilisation de l' approche d' annotation :

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

ANDLes Qobjets ed 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 )
Davor Lucic
la source
6
Existe-t-il une solution avec une recherche personnalisée? docs.djangoproject.com/en/1.10/howto/custom-lookups Ce serait cool de passer "__in" à "__all" et de lui faire créer la bonne requête SQL.
t1m0
1
Cette solution d'annotation semble fausse. Que faire s'il y a trois balises possibles (appelons la balise supplémentaire pour t3, et une photo a les balises t2et t3. Ensuite, cette photo correspondra toujours à la requête donnée.
beruic
@beruic Je pense que l'idée est de remplacer num_tags = 2 par num_tags = len (tags); Je suppose que le codé en dur 2 était juste pour l'amour de l'exemple.
tbm
3
@tbm Cela ne fonctionnerait toujours pas. Photo.objects.filter(tags__in=tags)correspond aux photos qui ont l'une des balises, pas seulement celles qui ont tout. Certains de ceux qui n'ont qu'une seule des balises souhaitées peuvent avoir exactement le nombre de balises que vous recherchez, et certains de ceux qui ont toutes les balises souhaitées peuvent également avoir des balises supplémentaires.
beruic
1
@beruic l'annotation ne compte que les balises renvoyées par la requête, donc si (num balises renvoyées par la requête) == (num balises recherchées) alors la ligne est incluse; Les balises "extra" ne sont pas recherchées, elles ne seront donc pas comptabilisées. J'ai vérifié cela dans ma propre application.
tbm
8

Une autre approche qui fonctionne, bien que PostgreSQL ™ uniquement, consiste à utiliser django.contrib.postgres.fields.ArrayField :

Exemple copié à partir de documents :

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

ArrayFieldpossède des fonctionnalités plus puissantes telles que le chevauchement et les transformations d'index .

Sander van Leeuwen
la source
3

Cela peut également être fait par la génération de requêtes dynamiques à l'aide de Django ORM et de la magie 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]))

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 en un seul QuerySet. Par exemple, pour votre exemple, ce serait égal à

res = Photo.filter(Q(tags__name='holiday') & Q(tags__name='summer'))
demalexx
la source
3
Cela ne marcherait pas. Vos exemples de requêtes ne renverraient rien pour les modèles en question.
Davor Lucic
Merci pour la correction. Je pensais que le chaînage filterserait la même chose que l'utilisation andd'objets Q dans un filtre ... Mon erreur.
demalexx
Pas de soucis, ma première pensée était aussi celle des objets Q.
Davor Lucic
1
Cela serait plus lent si vous travaillez avec de grandes tables et des données volumineuses à comparer. (comme 1 million chacun)
gies0r
Cette approche devrait fonctionner si vous passez de filterà excludeet utilisez un opérateur de négation. Comme ça: res = Photo.exclude(~reduce(and_, [Q(tags__name=c) for c in categories]))
Ben le
1

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 être appelée comme ça:

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

il fonctionne également avec n'importe quelle classe et plus de balises dans la liste; les opérateurs peuvent être n'importe qui comme 'iexact', 'in', 'contains', 'ne', ...

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

Si nous voulons le faire de manière dynamique, suivez l'exemple:

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
la source
Ne peut pas fonctionner dès la deuxième itération, le jeu de requêtes sera vide
lapin