8

Как объединить несколько QuerySet в Django?

1

Я пытаюсь реализовать поиск на сайте, построенном на Django, и в этом поиске я должен искать по трём различным моделям. Для пагинации результатов поиска я хотел бы использовать универсальное представление object_list для отображения результатов. Но для этого мне нужно объединить три QuerySet в один.

Как мне это сделать? Я пробовал следующее:

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

Но это не работает. Я получаю ошибку, когда пытаюсь использовать этот список в универсальном представлении. Список не имеет атрибута clone.

Как мне объединить три списка: page_list, article_list и post_list?

5 ответ(ов)

12

Конкатенация queryset'ов в список - это самый простой подход. Если к базе данных всё равно будет производиться обращение для всех queryset'ов (например, потому что результат необходимо отсортировать), это не добавит дополнительных затрат.

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

Использование itertools.chain работает быстрее, чем поэлементное добавление элементов из каждого списка, так как itertools реализован на C. Кроме того, это требует меньше памяти, чем преобразование каждого queryset в список перед конкатенацией.

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

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

Вы можете изменить порядок сортировки на обратный:

result_list = sorted(
    chain(page_list, article_list, post_list),
    key=attrgetter('date_created'),
    reverse=True,
)

attrgetter эквивалентен следующему lambda (так нужно было делать до Python 2.4):

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

Попробуйте это:

matches = pages | articles | posts

Это сохраняет все функции запросов, что полезно, если вам нужно использовать order_by или что-то подобное.

Обратите внимание: это не сработает с запросами из двух различных моделей.

0

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

Обратите внимание, что вам нужно указать template_name=, если вы используете QuerySetChain с обобщёнными представлениями, даже если все связанные подзапросы используют одну и ту же модель.

from itertools import islice, chain

class QuerySetChain(object):
    """
    Объединяет несколько подзапросов (возможно, разных моделей) и ведёт себя как
    один запрос. Поддерживает минимальный набор методов, необходимых для использования с
    django.core.paginator.
    """

    def __init__(self, *subquerysets):
        self.querysets = subquerysets

    def count(self):
        """
        Выполняет .count() для всех подзапросов и возвращает количество
        записей как целое число.
        """
        return sum(qs.count() for qs in self.querysets)

    def _clone(self):
        "Возвращает клон этой цепочки запросов"
        return self.__class__(*self.querysets)

    def _all(self):
        "Итерация по записям во всех подзапросах"
        return chain(*self.querysets)

    def __getitem__(self, ndx):
        """
        Извлекает элемент или срез из объединённого набора результатов всех
        подзапросов.
        """
        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.

0

Если вам нужно объединить множество queryset'ов, попробуйте следующее:

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

Где docs — это список queryset'ов.

0

Это можно сделать двумя способами.

1-й способ

Используйте оператор объединения для QuerySet |, чтобы взять объединение двух QuerySet. Если оба QuerySet принадлежат одной модели, то можно комбинировать их с помощью оператора объединения.

Например:

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  # это объединит два QuerySet

2-й способ

Еще один способ объединить операции между двумя QuerySet — использовать функцию chain из модуля itertools.

from itertools import chain
combined_results = list(chain(pagelist1, pagelist2))
Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь