risposta-alla-domanda-sullo-sviluppo-web-bd.com

Come combinare 2 o più querysets in una vista Django?

Sto cercando di costruire la ricerca di un sito Django che sto costruendo, e nella ricerca sto cercando in 3 modelli diversi. E per ottenere la paginazione nella lista dei risultati di ricerca vorrei utilizzare una vista elenco_oggetto generica per visualizzare i risultati. Ma per farlo devo unire 3 querysets in uno solo.

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 Ottengo un errore quando provo ad usare quell'elenco nella vista generica. Nell'elenco manca l'attributo clone.

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

594
espenhogbakk

Concatenare i querysets in una lista è l'approccio più semplice. Se il database verrà colpito per tutti i set di query comunque (ad esempio perché il risultato deve essere ordinato), ciò non aggiungerà ulteriori costi.

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

L'utilizzo di itertools.chain è più rapido del looping di ogni elenco e degli elementi di accodamento uno per uno, poiché itertools è implementato in C. Inoltre, consuma meno memoria rispetto alla conversione di ciascun queryset 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 funzione sorted() accetta comodamente un generatore e restituisce una lista:

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 attrgetter invece di un lambda. Ricordo di aver letto che era più veloce, ma non ho visto una differenza di velocità notevole per un elenco di milioni di elementi.

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

Prova questo:

matches = pages | articles | posts

Mantiene tutte le funzioni dei set di query che è Nice se si desidera order_by o simili.

Spiacenti, tieni presente che ciò non funziona sui set di query di due modelli diversi ...

414
bryan

Correlati, per mescolare i set di query dallo stesso modello, o per campi simili da alcuni modelli, A partire da Django 1.11 a qs.union() method è anche disponibile:

union()

union(*other_qs, all=False)

Novità 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 (), intersezione () e differenza () restituiscono le 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 sono importanti purché i tipi siano nello stesso ordine).

Inoltre, solo il LIMIT, OFFSET e ORDER BY (cioè slicing e order_by ()) sono consentiti sul QuerySet risultante. 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

91
Udi

Puoi utilizzare la classe QuerySetChain di seguito. Quando lo si utilizza con l'impaginatore di Django, dovrebbe solo colpire il database con le query COUNT(*) per tutte le query querysets e SELECT() solo per quei querysets i cui record sono visualizzati nella pagina corrente.

Si noti che è necessario specificare template_name= se si utilizza un QuerySetChain con viste generiche, anche se i querysets concatenati utilizzano tutti 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 usa matches con l'impaginatore come hai usato result_list nel tuo esempio.

Il modulo itertools è stato introdotto in Python 2.3, quindi dovrebbe essere disponibile in tutte le versioni di Python eseguite su Django.

73
akaihola

Il grosso svantaggio del tuo attuale approccio è la sua inefficienza con i set di risultati di ricerca di grandi dimensioni, poiché devi tirare giù l'intero set di risultati dal database ogni volta, anche se intendi solo visualizzare una pagina di risultati.

Per poter solo trascinare verso il basso gli oggetti effettivamente necessari dal database, è necessario utilizzare la paginazione su un QuerySet, non su un elenco. Se si esegue questa operazione, Django taglia effettivamente il QuerySet prima che la query venga eseguita, quindi la query SQL utilizzerà OFFSET e LIMIT per ottenere solo i record effettivamente visualizzati. Ma non puoi farlo a meno che tu non possa 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 ? È sufficiente che tutti e tre i modelli ereditino da un antenato comune con titolo e corpo e esegua la ricerca come singola query sul modello di antenato.

26
Carl Meyer

Nel caso in cui desideri concatenare un sacco di querysets, prova questo:

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

dove: docs è una lista di querysets

20
vutran
DATE_FIELD_MAPPING = {
    Model1: 'date',
    Model2: 'pubdate',
}

def my_key_func(obj):
    return getattr(obj, DATE_FIELD_MAPPING[type(obj)])

And then sorted(chain(Model1.objects.all(), Model2.objects.all()), key=my_key_func)

Citato da https://groups.google.com/forum/#!topic/Django-users/6wUNuJa4jVw . Vedi Alex Gaynor

15
ray6080

ecco un'idea ... tira giù una pagina intera di risultati da ciascuno dei tre e poi butta fuori i 20 meno utili ... questo elimina i grossi querysets e in questo modo sacrifichi solo un po 'di performance invece di molto

5
Jiaaro

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

Nel caso in cui si voglia combinare querysets e ancora uscire con un QuerySet, si potrebbe voler controllare Django-queryset-sequence .

Ma una nota a riguardo. Richiede solo due querysets come argomento. Ma con python reduce puoi 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 che ho incontrato e come ho impiegato list comprehension, reduce e Django-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})
5
chidimo

Questo può essere ottenuto in due modi.

1o modo per farlo

Utilizzare l'operatore unione per queryset | per ottenere l'unione di due queryset. Se entrambi i queryset appartengono allo stesso modello/singolo modello di quanto sia possibile combinare i querysets usando 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

2o modo per farlo

Un altro modo per ottenere l'operazione di combinazione tra due queryset consiste nell'usare itertools funzione chain.

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

Questa funzione ricorsiva concatena array di query in un 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