Как объединить несколько QuerySet в Django?
Я пытаюсь реализовать поиск на сайте, построенном на 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 ответ(ов)
Конкатенация 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,
)
Попробуйте это:
matches = pages | articles | posts
Это сохраняет все функции запросов, что полезно, если вам нужно использовать order_by
или что-то подобное.
Обратите внимание: это не сработает с запросами из двух различных моделей.
Вы можете использовать класс 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.
Если вам нужно объединить множество queryset'ов, попробуйте следующее:
from itertools import chain
result = list(chain(*docs))
Где docs
— это список queryset'ов.
Это можно сделать двумя способами.
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))
Как выполнить фильтрацию запросов в Django по условию "не равно"?
Существует ли список временных зон Pytz?
В чем разница между null=True и blank=True в Django?
Как отменить последнюю миграцию?
Как получить значения GET-запроса в Django?