Jak połączyć dwa lub więcej zestawów zapytań w widoku Django?

653

Próbuję zbudować wyszukiwanie dla witryny Django, którą tworzę, i podczas tego wyszukiwania szukam w 3 różnych modelach. Aby uzyskać paginację na liście wyników wyszukiwania, chciałbym użyć ogólnego widoku lista_obiektu do wyświetlenia wyników. Ale żeby to zrobić, muszę scalić 3 zestawy zapytań w jeden.

Jak mogę to zrobić? Próbowałem tego:

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

Ale to nie działa. Podczas próby użycia tej listy w widoku ogólnym pojawia się błąd. Na liście brakuje atrybutu klonowania.

Czy ktoś wie, jak mogę połączyć trzy listy page_list, article_listi post_list?

espenhogbakk
źródło
Wygląda na to, że t_rybik stworzył kompleksowe rozwiązanie na djangosnippets.org/snippets/1933
akaihola
Do wyszukiwania lepiej używać dedykowanych rozwiązań, takich jak Haystack - jest bardzo elastyczny.
minder
1
Użytkownicy Django 1.11 i abv, zobacz tę odpowiedź - stackoverflow.com/a/42186970/6003362
Sahil Agarwal
Uwaga : pytanie ogranicza się do bardzo rzadkiego przypadku, gdy po połączeniu 3 różnych modeli nie trzeba ponownie wyodrębniać modeli z listy, aby rozróżnić dane dotyczące typów. W większości przypadków - jeśli oczekiwane jest rozróżnienie - spowoduje to niepoprawny interfejs. Dla tych samych modeli: zobacz odpowiedzi na temat union.
Sławomir Lenart

Odpowiedzi:

1058

Łączenie zestawów zapytań w listę jest najprostszym podejściem. Jeśli baza danych i tak zostanie trafiona dla wszystkich zestawów zapytań (np. Ponieważ wynik musi zostać posortowany), nie spowoduje to dodatkowych kosztów.

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

Używanie itertools.chainjest szybsze niż zapętlanie każdej listy i dodawanie elementów jeden po drugim, ponieważ itertoolsjest zaimplementowane w C. Zużywa również mniej pamięci niż przekształcanie każdego zestawu zapytań w listę przed konkatenacją.

Teraz można posortować wynikową listę, np. Według daty (zgodnie z żądaniem w komentarzu hasen j do innej odpowiedzi). sorted()Funkcja korzystnie przyjmuje generator i zwraca listy:

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

Jeśli używasz attrgetterjęzyka Python 2.4 lub nowszego, możesz użyć zamiast lambda. Pamiętam, że czytałem o tym, że jest szybszy, ale nie zauważyłem zauważalnej różnicy prędkości na liście milionów przedmiotów.

from operator import attrgetter
result_list = sorted(
    chain(page_list, article_list, post_list),
    key=attrgetter('date_created'))
akaihola
źródło
13
Jeśli scalasz zestawy zapytań z tej samej tabeli, aby wykonać zapytanie OR i masz zduplikowane wiersze, możesz je wyeliminować za pomocą funkcji grupowania: from itertools import groupby unique_results = [rows.next() for (key, rows) in groupby(result_list, key=lambda obj: obj.id)]
Josh Russo,
1
Ok, więc nm o funkcji grupowania w tym kontekście. Dzięki funkcji Q powinieneś być w stanie wykonać dowolne zapytanie OR: https://docs.djangoproject.com/en/1.3/topics/db/queries/#complex-lookups-with-q-objects
Josh Russo
2
@apelliciari Chain zużywa znacznie mniej pamięci niż list.extend, ponieważ nie musi w pełni ładować obu list do pamięci.
Dan Gayle
2
@AWrightIV Oto nowa wersja tego linku: docs.djangoproject.com/en/1.8/topics/db/queries/…
Josh Russo
1
próbuję tego podejścia, ale grillazz'list' object has no attribute 'complex_filter'
2016
466

Spróbuj tego:

matches = pages | articles | posts

Zachowuje wszystkie funkcje zestawów zapytań, co jest miłe, jeśli chcesz order_bylub podobnie.

Uwaga: to nie działa na zestawach zapytań z dwóch różnych modeli.

Daniel Holmes
źródło
10
Jednak nie działa na pokrojone zestawy zapytań. A może coś mi brakuje?
sthzg
1
Dołączyłem do zestawów zapytań za pomocą „|” ale nie zawsze działa dobrze. Lepiej użyć „Q”: docs.djangoproject.com/en/dev/topics/db/queries/…
Ignacio Pérez
1
Wydaje się, że nie tworzy duplikatów przy użyciu Django 1.6.
Teekin,
15
Oto |operator ustawionego związku, nie bitowy LUB.
e100
6
@ e100 nie, to nie jest ustawiony operator unii. django przeciąża bitowego operatora OR: github.com/django/django/blob/master/django/db/models/…
shangxiao
109

Powiązane, do mieszania zestawów zapytań z tego samego modelu lub do podobnych pól z kilku modeli, począwszy od Django 1.11qs.union() metoda dostępna jest również:

union()

union(*other_qs, all=False)

Nowości w Django 1.11 . Używa operatora UNION języka SQL do łączenia wyników dwóch lub więcej zestawów QuerySets. Na przykład:

>>> qs1.union(qs2, qs3)

Operator UNION wybiera domyślnie tylko odrębne wartości. Aby zezwolić na powielanie wartości, użyj argumentu all = True.

union (), intersection () i Difference () zwracają instancje modelu typu pierwszego QuerySet, nawet jeśli argumentami są QuerySets innych modeli. Przekazywanie różnych modeli działa, o ile lista WYBIERZ jest taka sama we wszystkich zestawach QuerySets (przynajmniej typy, nazwy nie mają znaczenia tak długo, jak typy w tej samej kolejności).

Ponadto w wynikowym QuerySet są dozwolone tylko LIMIT, OFFSET i ORDER BY (tj. Krojenie i order_by ()). Ponadto bazy danych nakładają ograniczenia na dozwolone operacje w połączonych zapytaniach. Na przykład większość baz danych nie zezwala na LIMIT lub OFFSET w połączonych zapytaniach.

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

Udi
źródło
To lepsze rozwiązanie dla mojego zestawu problemów, który musi mieć unikalne wartości.
Burning Crystals,
Nie działa w przypadku geometrii geodjango.
MarMat
Skąd jednak importujesz związek? Czy musi pochodzić z jednego z X zestawów zapytań?
Jack
Tak, jest to metoda zestawu zapytań.
Udi
Myślę, że usuwa filtry wyszukiwania
Pierre Cordier,
76

Możesz skorzystać z QuerySetChainponiższej klasy. Używając go z paginatorem Django, powinien on trafić do bazy danych z COUNT(*)zapytaniami dla wszystkich zestawów SELECT()zapytań i zapytaniami tylko dla tych zestawów zapytań, których rekordy są wyświetlane na bieżącej stronie.

Pamiętaj, że musisz określić, template_name=czy używasz QuerySetChainwidoków ogólnych, nawet jeśli wszystkie powiązane zestawy zapytań używają tego samego modelu.

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

W twoim przykładzie użycie to:

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)

Następnie użyj matchesz paginatorem, tak jak result_listw przykładzie.

itertoolsModuł został wprowadzony w Pythonie 2.3, więc powinien on być dostępny we wszystkich wersjach Pythona Django biegnie dalej.

akaihola
źródło
5
Ładne podejście, ale jednym z problemów, które tu widzę, jest to, że zestawy zapytań są dołączane „od stóp do głów”. Co się stanie, jeśli każdy zestaw zapytań zostanie uporządkowany według daty, a zestaw połączony będzie również potrzebny według daty?
hasen
To z pewnością wygląda obiecująco, świetnie, muszę to wypróbować, ale dzisiaj nie mam czasu. Oddzwonię, jeśli to rozwiąże mój problem. Świetna robota.
espenhogbakk
Ok, musiałem dziś spróbować, ale to nie zadziałało, najpierw narzekało, że nie musi klonować atrybutu, więc dodałem ten, właśnie skopiowałem _all i to działało, ale wygląda na to, że paginator ma jakiś problem z tym zestawem zapytań. Pojawia się błąd paginatora: „len () obiektu bez rozmiaru”
espenhogbakk 11.01.2009
1
@Espen Biblioteka Python: pdb, logowanie. Zewnętrzne: IPython, ipdb, rejestrowanie django, pasek narzędzi debugowania django, rozszerzenia poleceń django, werkzeug. Użyj instrukcji drukowania w kodzie lub modułu rejestrującego. Przede wszystkim naucz się introspekcji w powłoce. Google dla blogów na temat debugowania Django. Miło, że mogłem pomóc!
akaihola
4
@patrick patrz djangosnippets.org/snippets/1103 i djangosnippets.org/snippets/1933 - szczególnie ten ostatni jest bardzo kompleksowym rozwiązaniem
akaihola
27

Dużym minusem obecnego podejścia jest jego nieskuteczność w przypadku dużych zestawów wyników wyszukiwania, ponieważ za każdym razem trzeba wyciągać cały zestaw wyników z bazy danych, nawet jeśli zamierza się wyświetlić tylko jedną stronę wyników.

Aby wyciągnąć tylko te obiekty, których naprawdę potrzebujesz z bazy danych, musisz użyć paginacji na QuerySet, a nie na liście. Jeśli to zrobisz, Django faktycznie wycina QuerySet przed wykonaniem zapytania, więc zapytanie SQL użyje OFFSET i LIMIT, aby uzyskać tylko rekordy, które faktycznie wyświetlisz. Ale nie możesz tego zrobić, chyba że w jakiś sposób możesz wcisnąć swoje wyszukiwanie w jedno zapytanie.

Biorąc pod uwagę, że wszystkie trzy modele mają pola tytułu i treści, dlaczego nie zastosować dziedziczenia modelu ? Wystarczy, że wszystkie trzy modele odziedziczą po wspólnym przodku, który ma tytuł i treść, i wykonaj wyszukiwanie jako pojedyncze zapytanie w modelu przodka.

Carl Meyer
źródło
23

Jeśli chcesz połączyć wiele zestawów zapytań, spróbuj tego:

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

gdzie: docs to lista zestawów zapytań

Vutran
źródło
8

Można to osiągnąć na dwa sposoby.

Pierwszy sposób to zrobić

Użyj operatora unii dla zestawu zapytań, |aby uzyskać połączenie dwóch zestawów zapytań. Jeśli oba zestawy zapytań należą do tego samego modelu / jednego modelu, możliwe jest połączenie zestawów zapytań za pomocą operatora unii.

Na przykład

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

Drugi sposób to zrobić

Innym sposobem osiągnięcia operacji łączenia między dwoma zestawami zapytań jest użycie funkcji łańcucha itertools .

from itertools import chain
combined_results = list(chain(pagelist1, pagelist2))
Devang Padhiyar
źródło
7

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

Jeśli chcesz połączyć querysetsi nadal wychodzić z QuerySet, możesz chcieć sprawdzić sekwencję django-queryset .

Ale jedna uwaga na ten temat. querysetsArgument wymaga tylko dwóch . Ale w Pythonie reducezawsze możesz zastosować go do wielu querysets.

from functools import reduce
from queryset_sequence import QuerySetSequence

combined_queryset = reduce(QuerySetSequence, list_of_queryset)

I to wszystko. Poniżej znajduje się sytuacja, na którą natknąłem się i jak to zrobiłem list comprehension, reduceorazdjango-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
źródło
1
Czy Book.objects.filter(owner__mentor=mentor)nie robi tego samego? Nie jestem pewien, czy jest to prawidłowy przypadek użycia. Myślę, że Bookmoże być konieczne podanie wielu owners, zanim zaczniesz robić coś takiego.
Czy S
Tak, robi to samo. Próbowałem tego. W każdym razie być może może to być przydatne w innej sytuacji. Dzięki za zwrócenie na to uwagi. Nie zaczynasz od znajomości wszystkich skrótów jako początkujący. Czasami trzeba jechać krętą drogą, aby docenić muchę
wrony
6

oto pomysł ... po prostu ściągnij jedną pełną stronę wyników z każdego z trzech, a następnie wyrzuć 20 najmniej przydatnych ... to eliminuje duże zestawy zapytań i w ten sposób poświęcasz tylko niewielką wydajność zamiast dużo

Jiaaro
źródło
1

Spowoduje to wykonanie pracy bez użycia żadnych innych bibliotek

result_list = list(page_list) + list(article_list) + list(post_list)
Satyam Faujdar
źródło
-1

Ta funkcja rekurencyjna łączy tablicę zestawów zapytań w jeden zestaw zapytań.

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
źródło
1
Jestem dosłownie zagubiony.
lycuid
łącząc wynik zapytania, którego nie można użyć w czasie wykonywania, i to naprawdę zły pomysł, aby to zrobić. ponieważ czasami dodaje duplikację nad wynikiem.
Devang Hingu