123 votes

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

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 veux filtrer les objets photo qui ont toutes les balises présentes dans la base de données. catégories .

J'ai essayé :

Photo.objects.filter(tags__name__in=categories)

Mais cela correspond tout dans les catégories, et non tous articles.

Ainsi, si les catégories sont ['vacances', 'été'], je veux des photos avec une étiquette vacances et été.

Est-ce possible ?

7 votes

Maybe : qs=Photo.objects.all() ; for category in categories : qs = qs.filter(tags__name=category)

2 votes

Jpic a raison, Photo.objects.filter(tags__name='holiday').filter(tags__name‌​='summer') est la voie à suivre. (C'est la même chose que l'exemple de jpic). Chaque filter devrait ajouter plus JOIN à interroger, donc vous pourriez prendre approche d'annotation s'ils sont trop nombreux.

1 votes

Voici la référence dans les docs : docs.djangoproject.com/fr/dev/topics/db/queries/

152voto

Davor Lucic Points 11072

Résumé :

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

Il y a le agrégation approche . Cette requête serait plus courte et peut-être plus rapide pour un grand nombre de catégories.

Vous avez également la possibilité d'utiliser les 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 :

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 annotation approche :

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 fonctionnerait 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 )

10voto

Une autre approche qui fonctionne, bien qu'elle ne concerne que PostgreSQL, est l'utilisation de 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 y Transformations d'indices .

5voto

demalexx Points 1871

Cela peut également être fait par la génération dynamique de requêtes en utilisant l'ORM de Django et un peu de 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 en utilisant l'opérateur AND dans un QuerySet. Par exemple, pour votre exemple, cela équivaudrait à

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

4 votes

Cela ne fonctionnerait pas. Vos exemples de requêtes ne renverraient rien pour les modèles en question.

0 votes

Merci pour la correction. Je pensais que l'enchaînement filter serait la même chose que d'utiliser and pour Q objets dans un filtre... C'est mon erreur.

0 votes

Pas de soucis, ma première pensée était aussi les objets Q.

2voto

Si vous avez été confronté à ce problème comme moi et que rien ne vous a aidé, peut-être que celui-ci résoudra votre problème.

Au lieu d'enchaîner les filtres, dans certains cas, il serait préférable de simplement stocker les identifiants des filtres précédents.

tags = [1, 2]
for tag in tags:
    ids = list(queryset.filter(tags__id=tag).values_list("id", flat=True))
    queryset = queryset.filter(id__in=ids)

En utilisant cette approche, vous éviterez de superposer JOIN dans la requête SQL :

1voto

David Points 292

J'utilise une petite fonction qui itère des 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 aussi avec n'importe quelle classe et plus de tags dans la liste ; les opérateurs peuvent être n'importe qui comme 'iexact', 'in', 'contains', 'ne',...

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X