Wie kombiniere ich zwei oder mehr Abfragesätze in einer Django-Ansicht?

653

Ich versuche, die Suche nach einer Django-Site zu erstellen, die ich erstelle, und bei dieser Suche suche ich in 3 verschiedenen Modellen. Und um eine Paginierung in der Suchergebnisliste zu erhalten, möchte ich eine generische Objektlistenansicht verwenden, um die Ergebnisse anzuzeigen. Aber dazu muss ich 3 Abfragesätze zu einem zusammenführen.

Wie kann ich das machen? Ich habe das versucht:

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

Das funktioniert aber nicht. Ich erhalte eine Fehlermeldung, wenn ich versuche, diese Liste in der allgemeinen Ansicht zu verwenden. In der Liste fehlt das Klonattribut.

Weiß jemand, wie ich die drei Listen zusammenführen kann page_list, article_listund post_list?

espenhogbakk
quelle
Es sieht so aus, als hätte t_rybik eine umfassende Lösung unter djangosnippets.org/snippets/1933
akaihola am
Für die Suche ist es besser, dedizierte Lösungen wie Haystack zu verwenden - es ist sehr flexibel.
Minder
1
Django Benutzer 1.11 und abv, siehe diese Antwort - stackoverflow.com/a/42186970/6003362
Sahil Agarwal
Hinweis : Die Frage beschränkt sich auf den sehr seltenen Fall, dass Sie nach dem Zusammenführen von 3 verschiedenen Modellen keine Modelle erneut aus der Liste extrahieren müssen, um Daten zu Typen zu unterscheiden. In den meisten Fällen - wenn eine Unterscheidung erwartet wird - wird die Schnittstelle falsch sein. Für die gleichen Modelle: siehe Antworten zu union.
Sławomir Lenart

Antworten:

1058

Das Verketten der Abfragesätze zu einer Liste ist der einfachste Ansatz. Wenn die Datenbank ohnehin für alle Abfragesätze getroffen wird (z. B. weil das Ergebnis sortiert werden muss), entstehen keine weiteren Kosten.

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

Die Verwendung itertools.chainist schneller als das Schleifen jeder Liste und das Anhängen von Elementen nacheinander, da dies itertoolsin C implementiert ist. Außerdem wird weniger Speicher benötigt als das Konvertieren jedes Abfragesatzes in eine Liste vor dem Verketten.

Jetzt ist es möglich, die resultierende Liste zB nach Datum zu sortieren (wie im Kommentar von hasen j zu einer anderen Antwort gefordert). Die sorted()Funktion akzeptiert bequem einen Generator und gibt eine Liste zurück:

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

Wenn Sie Python 2.4 oder höher verwenden, können Sie attrgetteranstelle eines Lambda verwenden. Ich erinnere mich, dass ich gelesen habe, dass es schneller ist, aber ich habe keinen merklichen Geschwindigkeitsunterschied für eine Million Artikel gesehen.

from operator import attrgetter
result_list = sorted(
    chain(page_list, article_list, post_list),
    key=attrgetter('date_created'))
Akaihola
quelle
13
Wenn Sie Abfragesätze aus derselben Tabelle zusammenführen, um eine ODER-Abfrage durchzuführen, und doppelte Zeilen haben, können Sie diese mit der groupby-Funktion entfernen: from itertools import groupby unique_results = [rows.next() for (key, rows) in groupby(result_list, key=lambda obj: obj.id)]
Josh Russo
1
Ok, also nm über die Groupby-Funktion in diesem Zusammenhang. Mit der Q-Funktion sollten Sie in der Lage sein, jede benötigte ODER-Abfrage auszuführen: https://docs.djangoproject.com/de/1.3/topics/db/queries/#complex-lookups-with-q-objects
Josh Russo
2
@apelliciari Chain benötigt deutlich weniger Speicher als list.extend, da nicht beide Listen vollständig in den Speicher geladen werden müssen.
Dan Gayle
2
@AWrightIV Hier ist die neue Version dieses Links: docs.djangoproject.com/de/1.8/topics/db/queries/…
Josh Russo
1
versuchen Sie diese Annäherung, aber haben'list' object has no attribute 'complex_filter'
Grillazz
466

Versuche dies:

matches = pages | articles | posts

Es behält alle Funktionen der Abfragesätze bei, was nett ist, wenn Sie wollen order_byoder ähnlich.

Bitte beachten Sie: Dies funktioniert nicht bei Abfragesätzen von zwei verschiedenen Modellen.

Daniel Holmes
quelle
10
Funktioniert jedoch nicht bei geschnittenen Abfragesätzen. Oder fehlt mir etwas?
sthzg
1
Ich habe die Abfragesätze mit "|" funktioniert aber nicht immer gut. Es ist besser, "Q" zu verwenden: docs.djangoproject.com/de/dev/topics/db/queries/…
Ignacio Pérez
1
Es scheint keine Duplikate mit Django 1.6 zu erstellen.
Teekin
15
Hier |ist der Set Union Operator, nicht bitweises ODER.
e100
6
@ e100 nein, es ist nicht der Set Union Operator. Django überlastet den bitweisen ODER-Operator: github.com/django/django/blob/master/django/db/models/…
shangxiao
109

Zum Mischen von Abfragesätzen desselben Modells oder für ähnliche Felder einiger weniger Modelle steht ab Django 1.11 auch eine qs.union()Methode zur Verfügung:

union()

union(*other_qs, all=False)

Neu in Django 1.11 . Verwendet den UNION-Operator von SQL, um die Ergebnisse von zwei oder mehr QuerySets zu kombinieren. Zum Beispiel:

>>> qs1.union(qs2, qs3)

Der UNION-Operator wählt standardmäßig nur unterschiedliche Werte aus. Verwenden Sie das Argument all = True, um doppelte Werte zuzulassen.

union (), intersection () und different () geben Modellinstanzen vom Typ des ersten QuerySet zurück, auch wenn die Argumente QuerySets anderer Modelle sind. Das Übergeben verschiedener Modelle funktioniert, solange die SELECT-Liste in allen QuerySets gleich ist (zumindest die Typen, die Namen spielen keine Rolle, solange die Typen in derselben Reihenfolge sind).

Darüber hinaus sind für das resultierende QuerySet nur LIMIT, OFFSET und ORDER BY (dh Slicing und order_by ()) zulässig. Darüber hinaus beschränken Datenbanken die zulässigen Vorgänge in den kombinierten Abfragen. Beispielsweise erlauben die meisten Datenbanken LIMIT oder OFFSET in den kombinierten Abfragen nicht.

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

Udi
quelle
Dies ist eine bessere Lösung für mein Problemset, das eindeutige Werte haben muss.
Brennende Kristalle
Funktioniert nicht für Geodjango-Geometrien.
MarMat
Woher importieren Sie Union? Muss es von einem der X Abfragesätze kommen?
Jack
Ja, es ist eine Methode zum Abfragen.
Udi
Ich denke, es entfernt Suchfilter
Pierre Cordier
76

Sie können die folgende QuerySetChainKlasse verwenden. Wenn Sie es mit Djangos Paginator verwenden, sollte es die Datenbank nur mit COUNT(*)Abfragen für alle Abfragesätze und SELECT()Abfragen nur für diejenigen Abfragesätze treffen, deren Datensätze auf der aktuellen Seite angezeigt werden.

Beachten Sie, dass Sie angeben müssen, template_name=ob Sie a QuerySetChainmit generischen Ansichten verwenden, auch wenn die verketteten Abfragesätze alle dasselbe Modell verwenden.

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

In Ihrem Beispiel wäre die Verwendung:

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)

Verwenden Sie dann matchesden Paginator wie result_listin Ihrem Beispiel.

Das itertoolsModul wurde in Python 2.3 eingeführt, daher sollte es in allen Python-Versionen verfügbar sein, auf denen Django ausgeführt wird.

Akaihola
quelle
5
Netter Ansatz, aber ein Problem, das ich hier sehe, ist, dass die Abfragesätze "von Kopf bis Schwanz" angehängt werden. Was ist, wenn jeder Abfragesatz nach Datum sortiert ist und der kombinierte Satz auch nach Datum sortiert werden muss?
Hasen
Das sieht auf jeden Fall vielversprechend aus, großartig, das muss ich versuchen, aber ich habe heute keine Zeit. Ich melde mich bei Ihnen, wenn es mein Problem löst. Gute Arbeit.
Espenhogbakk
Ok, ich musste es heute versuchen, aber es hat nicht funktioniert. Zuerst hat es sich beschwert, dass es kein _clone-Attribut haben muss, also habe ich dieses hinzugefügt, nur das _all kopiert und das hat funktioniert, aber es scheint, dass der Paginator ein Problem mit diesem Queryset hat. Ich erhalte diesen Paginatorfehler: "len () des nicht dimensionierten Objekts"
espenhogbakk
1
@ Espen Python-Bibliothek: pdb, Protokollierung. Extern: IPython, ipdb, Django-Protokollierung, Django-Debug-Symbolleiste, Django-Befehlserweiterungen, Werkzeug. Verwenden Sie print-Anweisungen im Code oder das Protokollierungsmodul. Lernen Sie vor allem, in der Schale nach innen zu schauen. Google für Blog-Beiträge zum Debuggen von Django. Froh, dass ich Helfen kann!
Akaihola
4
@patrick siehe djangosnippets.org/snippets/1103 und djangosnippets.org/snippets/1933 - insbesondere letzteres ist eine sehr umfassende Lösung
akaihola
27

Der große Nachteil Ihres aktuellen Ansatzes ist die Ineffizienz bei großen Suchergebnissen, da Sie jedes Mal die gesamte Ergebnismenge aus der Datenbank abrufen müssen, obwohl Sie nur eine Ergebnisseite anzeigen möchten.

Um nur die tatsächlich benötigten Objekte aus der Datenbank abzurufen, müssen Sie die Paginierung für ein QuerySet und nicht für eine Liste verwenden. Wenn Sie dies tun, schneidet Django das QuerySet tatsächlich in Scheiben, bevor die Abfrage ausgeführt wird, sodass die SQL-Abfrage OFFSET und LIMIT verwendet, um nur die Datensätze abzurufen, die Sie tatsächlich anzeigen. Sie können dies jedoch nur tun, wenn Sie Ihre Suche irgendwie in eine einzelne Abfrage packen können.

Warum nicht die Modellvererbung verwenden , da alle drei Modelle Titel- und Textfelder haben ? Lassen Sie einfach alle drei Modelle von einem gemeinsamen Vorfahren mit Titel und Text erben und führen Sie die Suche als einzelne Abfrage für das Vorfahrenmodell durch.

Carl Meyer
quelle
23

Wenn Sie viele Abfragesätze verketten möchten, versuchen Sie Folgendes:

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

Dabei gilt: docs ist eine Liste von Abfragesätzen

Vutran
quelle
8

Dies kann auch auf zwei Arten erreicht werden.

1. Weg dies zu tun

Verwenden Sie den Union-Operator für das Abfrageset |, um die Vereinigung von zwei Abfragesätzen zu übernehmen. Wenn beide Abfragesätze zu demselben Modell / Einzelmodell gehören, können Abfragesätze mithilfe des Union-Operators kombiniert werden.

Zum Beispiel

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. Weg dies zu tun

Eine andere Möglichkeit, eine Kombinationsoperation zwischen zwei Abfragesätzen zu erreichen, ist die Verwendung der itertools- Kettenfunktion.

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

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

Wenn Sie kombinieren querysetsund trotzdem mit a QuerySetherauskommen möchten , sollten Sie sich die Django-Queryset-Sequenz ansehen .

Aber eine Anmerkung dazu. Es braucht nur zwei querysetsals Argument. Aber mit Python können reduceSie es immer auf mehrere querysets anwenden .

from functools import reduce
from queryset_sequence import QuerySetSequence

combined_queryset = reduce(QuerySetSequence, list_of_queryset)

Und das ist es. Unten ist eine Situation, in die ich geraten bin und wie ich sie angestellt habe list comprehension, reduceunddjango-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})
Chidimo
quelle
1
Tut Book.objects.filter(owner__mentor=mentor)nicht das Gleiche? Ich bin nicht sicher, ob dies ein gültiger Anwendungsfall ist. Ich denke, ein muss Bookmöglicherweise mehrere owners haben, bevor Sie anfangen müssen, so etwas zu tun.
Will S
Ja, es macht das Gleiche. Ich versuchte es. Wie auch immer, vielleicht könnte dies in einer anderen Situation nützlich sein. Vielen Dank für den Hinweis. Als Anfänger kennen Sie nicht alle Abkürzungen. Manchmal muss man die kurvenreiche Straße
befahren
6

Hier ist eine Idee ... ziehen Sie einfach eine ganze Seite mit Ergebnissen aus jedem der drei heraus und werfen Sie dann die 20 am wenigsten nützlichen heraus ... dies eliminiert die großen Abfragesätze und auf diese Weise opfern Sie nur ein wenig Leistung statt viel

Jiaaro
quelle
1

Dies erledigt die Arbeit ohne Verwendung anderer Bibliotheken

result_list = list(page_list) + list(article_list) + list(post_list)
Satyam Faujdar
quelle
-1

Diese rekursive Funktion verkettet ein Array von Abfragesätzen zu einem Abfragesatz.

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
Petr Dvořáček
quelle
1
Ich bin buchstäblich verloren.
Lycuid
Wir kombinieren Abfrageergebnisse, die zur Laufzeit nicht verwendet werden können, und diese wirklich schlechte Idee, dies zu tun. weil es manchmal Duplizierung über Ergebnis hinzufügt.
Devang Hingu