Query di filtro Django __in per * ogni * elemento nell'elenco


102

Diciamo che ho i seguenti modelli

class Photo(models.Model):
    tags = models.ManyToManyField(Tag)

class Tag(models.Model):
    name = models.CharField(max_length=50)

In una vista ho un elenco con filtri attivi chiamati categorie . Voglio filtrare gli oggetti foto che hanno tutti i tag presenti nelle categorie .

Provai:

Photo.objects.filter(tags__name__in=categories)

Ma questo corrisponde a qualsiasi elemento nelle categorie, non a tutti gli elementi.

Quindi, se le categorie fossero ["vacanze", "estate"], voglio le foto con tag sia per le vacanze che per l'estate.

Può essere raggiunto?


7
Forse: qs = Photo.objects.all (); per la categoria nelle categorie: qs = qs.filter (tags__name = category)
jpic

2
jpic ha ragione, Photo.objects.filter(tags__name='holiday').filter(tags__name='summer')è la strada da percorrere. (Questo è lo stesso dell'esempio di jpic). Ognuno filterdovrebbe aggiungere più JOINmessaggi di posta elettronica alla query, quindi potresti adottare un approccio di annotazione se sono troppi.
Davor Lucic

1
Ecco il riferimento nella documentazione: docs.djangoproject.com/en/dev/topics/db/queries/…
sgallen

Ti aspetteresti che ci sia una funzione incorporata per questo da Django
Vincent

Risposte:


124

Sommario:

Un'opzione è, come suggerito da jpic e sgallen nei commenti, da aggiungere .filter()per ogni categoria. Ogni aggiunta filteraggiunge più join, il che non dovrebbe essere un problema per un piccolo insieme di categorie.

C'è l' approccio dell'aggregazione . Questa query sarebbe più breve e forse più veloce per un ampio insieme di categorie.

Hai anche la possibilità di utilizzare query personalizzate .


Qualche esempio

Configurazione di prova:

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

Utilizzando l' approccio dei filtri concatenati :

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

Query risultante:

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 )

Nota che ognuno filteraggiunge di più JOINSalla query.

Utilizzando l' approccio di annotazione :

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

Query risultante:

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

ANDgli Qoggetti ed non funzionerebbero:

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

Query risultante:

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 )

6
Esiste una soluzione con una ricerca personalizzata? docs.djangoproject.com/en/1.10/howto/custom-lookups Sarebbe bello cambiare "__in" in "__all" e creare la query sql corretta.
t1m0

1
Questa soluzione di annotazione sembra sbagliata. E se sono possibili tre tag (chiamiamo quello aggiuntivo per t3, e una foto ha i tag t2e t3. Allora questa foto corrisponderà ancora alla query data.
beruic

@beruic Penso che l'idea sia che dovresti sostituire num_tags = 2 con num_tags = len (tags); Mi aspetto che l'hard-coded 2 fosse solo per amore.
tbm

3
@tbm Ancora non funzionerebbe. Photo.objects.filter(tags__in=tags)corrisponde alle foto che hanno uno qualsiasi dei tag, non solo a quelle che hanno tutto. Alcuni di quelli che hanno solo uno dei tag desiderati, possono avere esattamente la quantità di tag che stai cercando, e alcuni di quelli che hanno tutti i tag desiderati, possono anche avere tag aggiuntivi.
beruic

1
@beruic l'annotazione conta solo i tag restituiti dalla query, quindi se (num tag restituiti dalla query) == (num tag ricercati) allora la riga è inclusa; I tag "extra" non vengono cercati, quindi non verranno conteggiati. L'ho verificato nella mia app.
tbm

8

Un altro approccio che funziona, sebbene solo PostgreSQL, sta usando django.contrib.postgres.fields.ArrayField:

Esempio copiato da documenti :

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

ArrayFieldha alcune funzionalità più potenti come la sovrapposizione e le trasformazioni di indice .


3

Questo può essere fatto anche mediante la generazione di query dinamiche usando Django ORM e un po 'di magia 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'idea è di generare oggetti Q appropriati per ciascuna categoria e quindi combinarli utilizzando l'operatore AND in un QuerySet. Ad esempio, per il tuo esempio sarebbe uguale a

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

3
Questo non funzionerebbe. Gli esempi di query non restituiranno nulla per i modelli in questione.
Davor Lucic

Grazie per la correzione. Pensavo che il concatenamento filtersarebbe stato lo stesso che usare andper oggetti Q in un filtro ... Errore mio.
demalexx

Nessun problema, il mio primo pensiero dove anche Q obietta.
Davor Lucic

1
Saremmo più lenti se lavorassi con tabelle di grandi dimensioni e dati di grandi dimensioni da confrontare. (come 1 milione ciascuno)
gies0r

Questo approccio dovrebbe funzionare se si passa da filtera excludee si utilizza un operatore di negazione. In questo modo: res = Photo.exclude(~reduce(and_, [Q(tags__name=c) for c in categories]))
Ben

1

Uso una piccola funzione che itera i filtri su un elenco per un dato operatore e un nome di colonna:

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  

e questa funzione può essere chiamata così:

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

funziona anche con qualsiasi classe e più tag nell'elenco; gli operatori possono essere chiunque come 'iexact', 'in', 'contains', 'ne', ...



-1

Se vogliamo farlo in modo dinamico, seguiamo l'esempio:

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

Non può funzionare non appena la seconda iterazione, il set di query sarà vuoto
lapin
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.