Як поєднати два чи більше набори запитів у режимі перегляду Джанго?


653

Я намагаюся створити пошук сайту Django, який я будую, і в цьому пошуку я шукаю в трьох різних моделях. І щоб отримати сторінку на сторінці результатів пошуку, я хотів би використовувати загальний вигляд object_list для відображення результатів. Але для цього мені потрібно об'єднати 3 запити в один.

Як я можу це зробити? Я спробував це:

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

Але це не працює. Я отримую помилку, коли намагаюся використовувати цей список у загальному вікні. У списку відсутній атрибут клонування.

Хто - небудь знає , як я можу об'єднати три списки, page_list, article_listі post_list?


Схоже, t_rybik створив комплексне рішення за адресою djangosnippets.org/snippets/1933
akaihola

Для пошуку краще використовувати спеціальні рішення, як Haystack - це дуже гнучко.
мислитель

1
Користувачі Django 1,11 і abv, дивіться цю відповідь - stackoverflow.com/a/42186970/6003362
Sahil Agarwal

Зауважте : питання обмежується дуже рідкісним випадком, коли після об'єднання трьох різних моделей разом вам не потрібно витягувати моделі в списку для розрізнення даних про типи. У більшості випадків - якщо очікується розрізнення - інтерфейс буде неправильним. Для тих же моделей: див. Відповіді про union.
Славомір Ленарт

Відповіді:


1058

Об’єднання наборів запитів у список є найпростішим підходом. Якщо база даних все одно буде потрапляти для всіх наборів запитів (наприклад, тому, що результат потрібно сортувати), це не призведе до додаткових витрат.

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

Використання itertools.chainшвидше, ніж циклічне записування кожного списку та додавання елементів один за одним, оскільки itertoolsреалізовано в C. Це також забирає менше пам'яті, ніж перетворення кожного набору запитів у список перед об'єднанням.

Тепер можна сортувати отриманий список, наприклад, за датою (як вимагається в коментарі hasen j до іншої відповіді). sorted()Функція зручно приймає генератор і повертає список:

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

Якщо ви використовуєте Python 2.4 або новішої версії, ви можете використовувати attrgetterзамість лямбда. Я пам'ятаю, як читав про це, як проходив швидше, але я не бачив помітної різниці швидкостей для списку мільйонів елементів.

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

14
Якщо об’єднувати набори запитів із тієї ж таблиці для виконання запиту АБО та мають дублювані рядки, ви можете усунути їх за допомогою функції groupby: from itertools import groupby unique_results = [rows.next() for (key, rows) in groupby(result_list, key=lambda obj: obj.id)]
Джош Руссо,

1
Гаразд, так нм про групову функцію в цьому контексті. За допомогою функції Q ви повинні мати змогу виконувати будь-який потрібний вам запит: https://docs.djangoproject.com/en/1.3/topics/db/queries/#complex-lookups-with-q-objects
Джош Руссо

2
@apelliciari Chain використовує значно менше пам'яті, ніж list.extend, тому що не потрібно повністю завантажувати обидва списки в пам'ять.
Ден Гейл

2
@AWrightIV Ось нова версія цього посилання: docs.djangoproject.com/en/1.8/topics/db/queries/…
Джош Руссо,

1
спробуйте цей підхід, але майте'list' object has no attribute 'complex_filter'
grillazz

466

Спробуйте це:

matches = pages | articles | posts

Він зберігає всі функції наборів запитів, що приємно, якщо ви хочете order_byчи подібні.

Зверніть увагу: це не працює на наборах запитів двох різних моделей.


10
Не працює на нарізаних наборах запитів. Або я щось пропускаю?
sthzg

1
Я приєднувався до наборів запитів за допомогою "|" але не завжди працює добре. Краще використовувати "Q": docs.djangoproject.com/en/dev/topics/db/queries/…
Ігнасіо Перес,

1
Здається, не створюється дублікатів, використовуючи Django 1.6.
Тікін

15
Тут |встановлений оператор об'єднання, а не порозрядне АБО.
e100

6
@ e100 ні, це не встановлений оператор об'єднання. django перевантажує побіт
shangxiao

109

Пов'язані, для змішування querysets з однієї і тієї ж моделі, або для аналогічних полів з декількох моделей, починаючи з Django 1.11 qs.union()метод також доступний:

union()

union(*other_qs, all=False)

Нове у Django 1.11 . Використовує оператор UNION SQL для об'єднання результатів двох або більше QuerySets. Наприклад:

>>> qs1.union(qs2, qs3)

Оператор UNION за замовчуванням вибирає лише окремі значення. Щоб дозволити повторювані значення, використовуйте аргумент all = True.

union (), intersection () та відмінності () повертають екземпляри моделі типу першого QuerySet, навіть якщо аргументами є QuerySets інших моделей. Передача різних моделей працює до тих пір, поки список SELECT однаковий у всіх QuerySets (принаймні типи, імена не мають значення, якщо типи в одному порядку).

Крім того, в отриманому QuerySet дозволені лише LIMIT, OFFSET та ORDER BY (тобто нарізання та order_by ()). Крім того, бази даних встановлюють обмеження щодо операцій, дозволених у комбінованих запитах. Наприклад, більшість баз даних не дозволяють LIMIT або OFFSET у комбінованих запитах.

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


Це краще рішення для мого набору проблем, який повинен мати унікальні значення.
Палаючі кристали

Не працює для геометрії geodjango.
MarMat

Звідки ви імпортуєте союз? Чи має він походити з одного з X числа запитів?
Джек

Так, це метод набору запитів.
Уді

Я думаю, що це видаляє фільтри пошуку
Pierre Cordier

76

Ви можете використовувати QuerySetChainклас нижче. Використовуючи його з пагінатором Django, він повинен звертатися до бази даних із COUNT(*)запитами для всіх наборів запитів і SELECT()запитів лише для тих наборів запитів, записи яких відображаються на поточній сторінці.

Зауважте, що вам потрібно вказати, template_name=якщо ви використовуєте QuerySetChainзагальні представлення даних, навіть якщо всі ланцюгові набори запитів використовують однакову модель.

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

У вашому прикладі використання буде:

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)

Тоді використовуйте matchesз пагінатором, як ви використовували result_listу своєму прикладі.

itertoolsМодуль був введений в Python 2.3, тому вона повинна бути доступна у всіх версіях Python Django працює під управлінням .


5
Хороший підхід, але тут я бачу одну проблему, що набори запитів додаються "головою до хвоста". Що робити, якщо кожен набір запитів упорядкований за датою, і потрібен комбінований набір, щоб він також був упорядкований за датою?
hasen

Це напевно виглядає багатообіцяюче, чудово, мені доведеться спробувати це, але я не маю часу сьогодні. Я повернуся до вас, якщо це вирішить мою проблему. Чудова робота.
espenhogbakk

Добре, я повинен був спробувати сьогодні, але це не спрацювало, спершу він поскаржився, що він не повинен мати атрибут _clone, тому я додав, що один, просто скопіював _all, і це спрацювало, але здається, що у пагінатора є певна проблема з цим запитом. Я отримую цю помилку у виправнику: "len ()
нерозмірного

1
@Espen Python library: pdb, ведення журналів. Зовнішні: IPython, ipdb, django-logging, django-debug-toolbar, django-command-extensions, werkzeug. Використовуйте оператори друку в коді або використовуйте модуль реєстрації. Перш за все, навчіться самоаналізу в оболонці. Google для публікацій у блозі про налагодження Django. Радий допомогти!
akaihola

4
@patrick див. djangosnippets.org/snippets/1103 та djangosnippets.org/snippets/1933 - особливо це останнє дуже комплексне рішення
akaihola

27

Важливим недоліком вашого поточного підходу є його неефективність із великими наборами результатів пошуку, оскільки вам доведеться щоразу знімати весь набір результатів із бази даних, навіть якщо ви маєте намір відображати лише одну сторінку результатів.

Для того, щоб витягнути з бази даних лише ті об’єкти, які вам справді потрібні, вам потрібно використовувати пагинацію в QuerySet, а не в списку. Якщо ви це зробите, Django фактично нарізає QuerySet перед виконанням запиту, тому SQL-запит використовуватиме OFFSET та LIMIT, щоб отримати лише записи, які ви будете фактично відображати. Але ви не можете цього зробити, якщо не зможете якимось чином забити свій пошук в одному запиті.

Враховуючи, що всі три ваші моделі мають поля заголовка та тіла, чому б не використати успадкування моделі ? Просто всі три моделі успадковують від загального предка, який має назву та тіло, і виконують пошук як єдиний запит на моделі предка.


23

Якщо ви хочете зав'язати багато наборів запитів, спробуйте:

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

де: docs - це список запитів



8

Цього можна досягти двома способами.

1-й спосіб зробити це

Використовуйте оператор об'єднання для набору запитів, |щоб прийняти об'єднання двох запитів. Якщо обидва набори запитів належать до однієї і тієї ж моделі / однієї моделі, тоді можливо комбінувати набори запитів за допомогою оператора об'єднання.

Наприклад

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-й спосіб зробити це

Ще один спосіб досягти комбінованої роботи між двома наборами запитів - використовувати функцію ланцюга itertools .

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

7

Вимоги: Django==2.0.2 ,django-querysetsequence==0.8

У випадку, якщо ви хочете поєднати querysetsі все-таки вийти з a QuerySet, ви можете перевірити послідовність django-queryset .

Але одна примітка про це. Це querysetsаргументується лише два . Але за допомогою python reduceви завжди можете застосувати його до кількох querysets.

from functools import reduce
from queryset_sequence import QuerySetSequence

combined_queryset = reduce(QuerySetSequence, list_of_queryset)

І це все. Нижче наводиться ситуація, в яку я стикався list comprehension, reduceі як я працював , і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})

1
Хіба Book.objects.filter(owner__mentor=mentor)не робити те ж саме? Я не впевнений, що це дійсний варіант використання. Я думаю, що вам Bookможе знадобитися мати кілька owners, перш ніж вам потрібно було робити щось подібне.
Буде S

Так, це робить те саме. Я спробував це. У будь-якому випадку, можливо, це може бути корисним у якійсь іншій ситуації. Дякуємо, що вказали на це. Ви точно не починаєте, знаючи всі ярлики як початківець. Іноді вам доведеться проїхати звивисту дорогу, щоб оцінити муху ворони
chidimo

6

ось ідея ... просто зніміть одну повну сторінку результатів з кожної з цих трьох, а потім викиньте 20 найменш корисних ... це виключає великі запити, і таким чином ви жертвуєте лише невеликою продуктивністю замість великої кількості


1

Це зробить роботу, не використовуючи жодних інших петель

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

-1

Ця рекурсивна функція об'єднує масив наборів запитів в один набір запитів.

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
Я буквально загублений.
лікуїд

ми поєднуємо результат запиту, його не можна використовувати під час виконання, і це дуже погана ідея для цього. тому що колись це додасть дублювання над результатом.
Деванг Хінгу
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.