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?


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

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.


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


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.


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.


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ń



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

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

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


1

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

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

-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

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.