Come combinare 2 o più queryset in una vista Django?
-
08-07-2019 - |
Domanda
Sto cercando di costruire la ricerca di un sito Django che sto costruendo, e nella 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 posso farlo? 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 a 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
?
Soluzione
La concatenazione dei queryset in un elenco è l'approccio più semplice. Se il database verrà 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 di itertools.chain
è più veloce del loop di ogni elenco e dell'aggiunta di elementi uno per uno, poiché itertools
è implementato in C. Inoltre, consuma meno memoria rispetto alla conversione di ogni query in un elenco prima di concatenare.
Ora è possibile ordinare l'elenco risultante, ad es. per data (come richiesto nel commento di hasen j ad un'altra risposta). La funzione sort ()
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 attrgetter
invece 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'))
Altri suggerimenti
Prova questo:
matches = pages | articles | posts
Mantiene tutte le funzioni dei queryset, il che è utile se si desidera order_by
o simili.
Nota: questo non funziona su queryset di due diversi modelli.
Correlato, per il mix di queryset dello stesso modello o per campi simili di alcuni modelli, a partire da Django 1.11 a qs.union ()
è 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. Ad esempio:
>>> qs1.union(qs2, qs3)
L'operatore UNION seleziona solo valori distinti per impostazione predefinita. Per consentire valori duplicati, utilizzare all = True argomento.
union (), intersection (), eference () restituiscono istanze del modello di il tipo del primo QuerySet anche se gli argomenti sono QuerySet di altri modelli. Il passaggio di diversi modelli funziona finché SELEZIONA elenco è lo stesso in tutti i QuerySet (almeno i tipi, i nomi no importa purché i tipi siano nello stesso ordine).
Inoltre, solo LIMIT, OFFSET e ORDER BY (ovvero affettatura e order_by ()) sono consentiti nel QuerySet risultante. Inoltre, database porre restrizioni su quali operazioni sono consentite nel combinato query. Ad esempio, la maggior parte dei database non ammette LIMIT o OFFSET in le query combinate.
https: / /docs.djangoproject.com/en/1.11/ref/models/querysets/#django.db.models.query.QuerySet.union
Puoi utilizzare la classe QuerySetChain
di seguito. Quando lo si utilizza con il paginator di Django, dovrebbe colpire il database solo con le query COUNT (*)
per tutte le query e le query SELECT ()
solo per quelle query i cui record sono visualizzati su la pagina corrente.
Nota che devi specificare template_name =
se usi un QuerySetChain
con viste generiche, anche se tutti i queryset concatenati usano 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 corrispondenze
con il paginatore 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 su cui Django gira.
Il grande svantaggio del vostro attuale approccio è la sua inefficienza con grandi set di risultati di ricerca, poiché ogni volta è necessario rimuovere l'intero set di risultati dal database, anche se si intende 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 raggruppare 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 eredità 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.
Nel caso in cui desideri concatenare molti queryset, prova questo:
from itertools import chain
result = list(chain(*docs))
dove: docs è un elenco di queryset
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 : agli utenti / 6wUNuJa4jVw . Vedi Alex Gaynor
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 invece di molto
Requisiti:
Django == 2.0.2
, django-querysetsequence==0.8
Nel caso in cui si desideri combinare queryset
e uscire comunque con un QuerySet
, si consiglia di controllare sequenza di django-queryset .
Ma una nota a riguardo. Ci vogliono solo due queryset
come argomento. Ma con Python riduci
puoi sempre applicarlo a più queryset
.
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 comprensione delle liste
, riduci
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})
Questo può essere ottenuto in due modi.
1o modo per farlo
Usa l'operatore union per il queryset |
per prendere 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
Secondo modo per farlo
Un altro modo per ottenere l'operazione di combinazione tra due queryset è utilizzare la funzione a catena itertools .
from itertools import chain
combined_results = list(chain(pagelist1, pagelist2))
Questa funzione ricorsiva concatena la matrice 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