Come combinare due o più queryset in una vista Django?


654

Sto cercando di costruire la ricerca di un sito Django che sto costruendo, e in quella ricerca, sto cercando in 3 diversi modelli. E per ottenere l'impaginazione nell'elenco dei risultati della ricerca, vorrei utilizzare una visualizzazione generica object_list per visualizzare i risultati. Ma per farlo, devo unire 3 queryset in uno.

Come lo posso fare? Ho provato questo:

result_list = []            
page_list = Page.objects.filter(
    Q(title__icontains=cleaned_search_term) | 
    Q(body__icontains=cleaned_search_term))
article_list = Article.objects.filter(
    Q(title__icontains=cleaned_search_term) | 
    Q(body__icontains=cleaned_search_term) | 
    Q(tags__icontains=cleaned_search_term))
post_list = Post.objects.filter(
    Q(title__icontains=cleaned_search_term) | 
    Q(body__icontains=cleaned_search_term) | 
    Q(tags__icontains=cleaned_search_term))

for x in page_list:
    result_list.append(x)
for x in article_list:
    result_list.append(x)
for x in post_list:
    result_list.append(x)

return object_list(
    request, 
    queryset=result_list, 
    template_object_name='result',
    paginate_by=10, 
    extra_context={
        'search_term': search_term},
    template_name="search/result_list.html")

Ma questo non funziona. Viene visualizzato un errore quando provo a utilizzare tale elenco nella visualizzazione generica. Nell'elenco manca l'attributo clone.

Qualcuno sa come posso unire le tre liste page_list, article_liste post_list?


Sembra che t_rybik abbia creato una soluzione completa su djangosnippets.org/snippets/1933
akaihola

Per la ricerca è meglio utilizzare soluzioni dedicate come Haystack : è molto flessibile.
minder,

1
Utenti Django 1.11 e abv, vedere questa risposta - stackoverflow.com/a/42186970/6003362
Sahil Agarwal

nota : la domanda si limita al caso molto raro quando dopo aver unito 3 modelli diversi non è necessario estrarre nuovamente i modelli nell'elenco per distinguere i dati sui tipi. Nella maggior parte dei casi, se si prevede una distinzione, l'interfaccia verrà errata. Per gli stessi modelli: vedi le risposte su union.
Sławomir Lenart,

Risposte:


1058

Concatenare i queryset in un elenco è l'approccio più semplice. Se il database verrà comunque colpito per tutti i queryset (ad es. Perché il risultato deve essere ordinato), ciò non comporterà ulteriori costi.

from itertools import chain
result_list = list(chain(page_list, article_list, post_list))

L'uso itertools.chainè più veloce del ciclo di ogni elenco e l'aggiunta di elementi uno alla volta, poiché itertoolsè implementato in C. Inoltre, consuma meno memoria rispetto alla conversione di ciascun gruppo di query in un elenco prima della concatenazione.

Ora è possibile ordinare l'elenco risultante ad es. Per data (come richiesto nel commento di hasen j ad un'altra risposta). La sorted()funzione accetta convenientemente un generatore e restituisce un elenco:

result_list = sorted(
    chain(page_list, article_list, post_list),
    key=lambda instance: instance.date_created)

Se stai usando Python 2.4 o successivo, puoi usare al attrgetterposto di un lambda. Ricordo di aver letto che era più veloce, ma non ho notato una notevole differenza di velocità per un milione di articoli.

from operator import attrgetter
result_list = sorted(
    chain(page_list, article_list, post_list),
    key=attrgetter('date_created'))

14
Se si uniscono queryset dalla stessa tabella per eseguire una query OR e si hanno righe duplicate, è possibile eliminarle con la funzione groupby: from itertools import groupby unique_results = [rows.next() for (key, rows) in groupby(result_list, key=lambda obj: obj.id)]
Josh Russo,

1
Ok, quindi nm sulla funzione groupby in questo contesto. Con la funzione Q dovresti essere in grado di eseguire qualsiasi query OR di cui hai bisogno: https://docs.djangoproject.com/en/1.3/topics/db/queries/#complex-lookups-with-q-objects
Josh Russo

2
@apelliciari Chain utilizza una quantità di memoria significativamente inferiore rispetto a list.extend, perché non è necessario caricare entrambi gli elenchi completamente in memoria.
Dan Gayle,

2
@AWrightIV Ecco la nuova versione di quel link: docs.djangoproject.com/en/1.8/topics/db/queries/…
Josh Russo,

1
provando questo approccio ma hai'list' object has no attribute 'complex_filter'
grillazz

466

Prova questo:

matches = pages | articles | posts

Mantiene tutte le funzioni dei queryset, il che è utile se lo si desidera order_byo simile.

Nota: questo non funziona su queryset di due diversi modelli.


10
Tuttavia, non funziona su queryset affettati. Oppure mi sfugge qualcosa?
sthzg

1
Mi univo ai queryset usando "|" ma non sempre funziona bene. È meglio usare "Q": docs.djangoproject.com/en/dev/topics/db/queries/…
Ignacio Pérez,

1
Non sembra creare duplicati, usando Django 1.6.
Teekin,

15
Ecco |l'operatore set unione, non bit a bit OR.
e100,

6
@ e100 no, non è l'operatore union set. django sovraccarica l'operatore bit per bit OR: github.com/django/django/blob/master/django/db/models/…
shangxiao

109

Correlato, per il mix di queryset dello stesso modello o per campi simili di alcuni modelli, A partire da Django 1.11 è disponibile anche un qs.union()metodo :

union()

union(*other_qs, all=False)

Nuovo in Django 1.11 . Utilizza l'operatore UNION di SQL per combinare i risultati di due o più QuerySet. Per esempio:

>>> qs1.union(qs2, qs3)

L'operatore UNION seleziona solo valori distinti per impostazione predefinita. Per consentire valori duplicati, utilizzare l'argomento all = True.

union (), intersection () e differenza () restituiscono istanze del modello del tipo del primo QuerySet anche se gli argomenti sono QuerySet di altri modelli. Il passaggio di modelli diversi funziona fintanto che l'elenco SELECT è lo stesso in tutti i QuerySet (almeno i tipi, i nomi non contano finché i tipi nello stesso ordine).

Inoltre, sul QuerySet risultante sono consentiti solo LIMIT, OFFSET e ORDER BY (ovvero slicing e order_by ()). Inoltre, i database pongono restrizioni su quali operazioni sono consentite nelle query combinate. Ad esempio, la maggior parte dei database non consente LIMIT o OFFSET nelle query combinate.

https://docs.djangoproject.com/en/1.11/ref/models/querysets/#django.db.models.query.QuerySet.union


Questa è una soluzione migliore per il mio set di problemi che deve avere valori univoci.
Burning Crystals,

Non funziona per le geometrie di geodjango.
MarMat

Da dove importa il sindacato? Deve provenire da un numero X di queryset?
Jack,

Sì, è un metodo di queryset.
Udi

Penso che rimuova i filtri di ricerca
Pierre Cordier,

76

Puoi usare la QuerySetChainclasse qui sotto. Quando lo si utilizza con il paginator di Django, dovrebbe colpire il database solo con COUNT(*)query per tutti i queryset eSELECT() query solo per quei queryset i cui record sono visualizzati nella pagina corrente.

Si noti che è necessario specificare template_name=se si utilizza a QuerySetChaincon viste generiche, anche se tutti i queryset concatenati utilizzano lo stesso modello.

from itertools import islice, chain

class QuerySetChain(object):
    """
    Chains multiple subquerysets (possibly of different models) and behaves as
    one queryset.  Supports minimal methods needed for use with
    django.core.paginator.
    """

    def __init__(self, *subquerysets):
        self.querysets = subquerysets

    def count(self):
        """
        Performs a .count() for all subquerysets and returns the number of
        records as an integer.
        """
        return sum(qs.count() for qs in self.querysets)

    def _clone(self):
        "Returns a clone of this queryset chain"
        return self.__class__(*self.querysets)

    def _all(self):
        "Iterates records in all subquerysets"
        return chain(*self.querysets)

    def __getitem__(self, ndx):
        """
        Retrieves an item or slice from the chained set of results from all
        subquerysets.
        """
        if type(ndx) is slice:
            return list(islice(self._all(), ndx.start, ndx.stop, ndx.step or 1))
        else:
            return islice(self._all(), ndx, ndx+1).next()

Nel tuo esempio, l'utilizzo sarebbe:

pages = Page.objects.filter(Q(title__icontains=cleaned_search_term) |
                            Q(body__icontains=cleaned_search_term))
articles = Article.objects.filter(Q(title__icontains=cleaned_search_term) |
                                  Q(body__icontains=cleaned_search_term) |
                                  Q(tags__icontains=cleaned_search_term))
posts = Post.objects.filter(Q(title__icontains=cleaned_search_term) |
                            Q(body__icontains=cleaned_search_term) | 
                            Q(tags__icontains=cleaned_search_term))
matches = QuerySetChain(pages, articles, posts)

Quindi utilizzare matchescon il paginatore come si usavaresult_list nel tuo esempio.

Il itertoolsmodulo è stato introdotto in Python 2.3, quindi dovrebbe essere disponibile in tutte le versioni di Python su cui gira Django.


5
Un approccio gradevole, ma un problema che vedo qui è che i set di query vengono aggiunti "testa a coda". Che cosa succede se ogni serie di query è ordinata per data e è necessario che anche l'insieme combinato sia ordinato per data?
hasen

Sembra decisamente promettente, fantastico, dovrò provarlo, ma oggi non ho tempo. Ti risponderò se risolverà il mio problema. Ottimo lavoro.
espenhogbakk,

Ok, ho dovuto provare oggi, ma non ha funzionato, prima si è lamentato del fatto che non doveva attribuire _clone l'attributo, quindi l'ho aggiunto, ho appena copiato il _all e ha funzionato, ma sembra che il paginatore abbia qualche problema con questo queryset. Ottengo questo errore impaginatore: "len () dell'oggetto non dimensionato"
espenhogbakk,

1
Libreria @Espen Python: pdb, registrazione. Esterno: IPython, ipdb, django-logging, django-debug-toolbar, django-command-extensions, werkzeug. Utilizzare le istruzioni di stampa nel codice o utilizzare il modulo di registrazione. Soprattutto, impara a introspeggiare nella shell. Google per i post sul blog sul debug di Django. Felice di aiutare!
Akaihola,

4
@patrick see djangosnippets.org/snippets/1103 e djangosnippets.org/snippets/1933 - soprattutto quest'ultima è una soluzione molto completa
Akaihola,

27

Il grande svantaggio del tuo attuale approccio è la sua inefficienza con grandi set di risultati di ricerca, in quanto devi rimuovere ogni volta l'intero set di risultati dal database, anche se hai intenzione di visualizzare solo una pagina di risultati.

Per estrarre solo gli oggetti effettivamente necessari dal database, è necessario utilizzare l'impaginazione su un QuerySet, non su un elenco. Se lo fai, Django effettivamente suddivide il QuerySet prima dell'esecuzione della query, quindi la query SQL utilizzerà OFFSET e LIMIT per ottenere solo i record che verranno effettivamente visualizzati. Ma non puoi farlo a meno che tu non riesca a stipare la tua ricerca in una singola query in qualche modo.

Dato che tutti e tre i tuoi modelli hanno campi titolo e corpo, perché non utilizzare l' ereditarietà del modello ? Basta avere tutti e tre i modelli ereditati da un antenato comune con titolo e corpo ed eseguire la ricerca come una singola query sul modello antenato.


23

Nel caso in cui desideri concatenare molti queryset, prova questo:

from itertools import chain
result = list(chain(*docs))

dove: docs è un elenco di query



8

Questo può essere ottenuto in due modi.

1o modo per farlo

Utilizzare l'operatore union per queryset |per eseguire l'unione di due queryset. Se entrambi i queryset appartengono allo stesso modello / modello singolo di quanto sia possibile combinare i queryset utilizzando l'operatore union.

Per un'istanza

pagelist1 = Page.objects.filter(
    Q(title__icontains=cleaned_search_term) | 
    Q(body__icontains=cleaned_search_term))
pagelist2 = Page.objects.filter(
    Q(title__icontains=cleaned_search_term) | 
    Q(body__icontains=cleaned_search_term))
combined_list = pagelist1 | pagelist2 # this would take union of two querysets

2 ° modo per farlo

Un altro modo per ottenere un'operazione di combinazione tra due queryset è utilizzare la funzione catena itertools .

from itertools import chain
combined_results = list(chain(pagelist1, pagelist2))

7

Requisiti: Django==2.0.2 ,django-querysetsequence==0.8

Nel caso in cui si desideri combinare querysetse ancora uscire con un QuerySet, potresti voler dare un'occhiata a django-queryset-sequence .

Ma una nota a riguardo. Ne bastano due querysetscome argomento. Ma con Python reducepuoi sempre applicarlo a più querysets.

from functools import reduce
from queryset_sequence import QuerySetSequence

combined_queryset = reduce(QuerySetSequence, list_of_queryset)

E questo è tutto. Di seguito è una situazione in cui mi sono imbattuto e come ho impiegato list comprehension, reduceedjango-queryset-sequence

from functools import reduce
from django.shortcuts import render    
from queryset_sequence import QuerySetSequence

class People(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    mentor = models.ForeignKey('self', null=True, on_delete=models.SET_NULL, related_name='my_mentees')

class Book(models.Model):
    name = models.CharField(max_length=20)
    owner = models.ForeignKey(Student, on_delete=models.CASCADE)

# as a mentor, I want to see all the books owned by all my mentees in one view.
def mentee_books(request):
    template = "my_mentee_books.html"
    mentor = People.objects.get(user=request.user)
    my_mentees = mentor.my_mentees.all() # returns QuerySet of all my mentees
    mentee_books = reduce(QuerySetSequence, [each.book_set.all() for each in my_mentees])

    return render(request, template, {'mentee_books' : mentee_books})

1
Non Book.objects.filter(owner__mentor=mentor)fa la stessa cosa? Non sono sicuro che si tratti di un caso d'uso valido. Penso che Bookpotrebbe essere necessario avere più owners prima che tu abbia bisogno di iniziare a fare qualcosa del genere.
Will S

Sì, fa la stessa cosa. L'ho provato. Comunque, forse questo potrebbe essere utile in qualche altra situazione. Grazie per la segnalazione. Non inizi esattamente conoscendo tutte le scorciatoie da principiante. A volte devi percorrere la tortuosa strada tortuosa per apprezzare la mosca del corvo
chidimo

6

ecco un'idea ... basta tirare giù una pagina intera di risultati da ciascuno dei tre e poi buttare via i 20 meno utili ... questo elimina i queryset di grandi dimensioni e in questo modo sacrifichi solo un po 'di prestazioni anziché molte


1

Questo farà il lavoro senza usare altre librerie

result_list = list(page_list) + list(article_list) + list(post_list)

-1

Questa funzione ricorsiva concatena l'array di queryset in un solo queryset.

def merge_query(ar):
    if len(ar) ==0:
        return [ar]
    while len(ar)>1:
        tmp=ar[0] | ar[1]
        ar[0]=tmp
        ar.pop(1)
        return ar

1
Sono letteralmente perso.
lycuid

stiamo combinando il risultato della query che non può essere utilizzato in fase di esecuzione e quella pessima idea di farlo. perché a volte si aggiunge la duplicazione al risultato.
Devang Hingu,
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.