Как объединить 2 или более запросов в представлении Django?
Я пытаюсь построить поиск для сайта Django, который я строю, и в поиске я ищу в 3 разных моделях. И чтобы получить нумерацию страниц в списке результатов поиска, я хотел бы использовать общее представление 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")
Но это не работает, я получаю сообщение об ошибке, когда пытаюсь использовать этот список в общем виде. В списке отсутствует атрибут clone.
Кто-нибудь знает, как я могу объединить три списка, page_list
, article_list
и post_list
?
Ответы
Ответ 1
Объединение запросов в список является самым простым подходом. Если база данных будет удалена для всех запросов в любом случае (например, потому что результат нужно сортировать), это не добавит дополнительных затрат.
from itertools import chain
result_list = list(chain(page_list, article_list, post_list))
Использование itertools.chain
выполняется быстрее, чем цикл каждого списка и добавление элементов один за другим, поскольку itertools
реализован в C. Он также потребляет меньше памяти, чем преобразование каждого набора запросов в список перед конкатенацией.
Теперь можно отсортировать полученный список, например. по дате (по запросу в jen 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'))
Ответ 2
Попробуй это:
matches = pages | articles | posts
Он сохраняет все функции наборов запросов, что хорошо, если вы хотите order_by
или аналогичный.
Обратите внимание: это не работает на наборах запросов от двух разных моделей.
Ответ 3
Связанный, для смешивания запросов с той же модели или для похожих полей из нескольких моделей, Начиная с Django 1.11 a qs.union()
:
union()
union(*other_qs, all=False)
Новое в Django 1.11. Использует оператор UNION SQLs для объединения результатов двух или более запросов QuerySets. Например:
>>> qs1.union(qs2, qs3)
Оператор UNION по умолчанию выбирает только разные значения. Чтобы разрешить повторяющиеся значения, используйте значение all = True аргумент.
union(), пересечение() и разность() возвращают экземпляры модели тип первого QuerySet, даже если аргументы QuerySets другие модели. Передача разных моделей работает до тех пор, пока SELECT список одинаковый во всех QuerySets (по крайней мере, типы, имена dont до тех пор, пока типы в том же порядке).
Кроме того, только LIMIT, OFFSET и ORDER BY (то есть нарезка и order_by()) разрешены на полученном QuerySet. Кроме того, базы данных устанавливать ограничения на то, какие операции разрешены в комбинированных запросов. Например, большинство баз данных не разрешают LIMIT или OFFSET в объединенные запросы.
https://docs.djangoproject.com/en/1.11/ref/models/querysets/#django.db.models.query.QuerySet.union
Ответ 4
Вы можете использовать класс QuerySetChain
ниже. При использовании его с paginator 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
с помощью paginator, как вы использовали result_list
в вашем примере.
Модуль itertools
был введен в Python 2.3, поэтому он должен быть доступен во всех версиях Django на Python.
Ответ 5
Большой недостаток вашего нынешнего подхода - его неэффективность с большими наборами результатов поиска, так как каждый раз приходится вытаскивать весь результирующий набор из базы данных, хотя вы только собираетесь отображать одну страницу результатов.
Чтобы только вытащить нужные вам объекты из базы данных, вам нужно использовать разбиение на страницы в QuerySet, а не на список. Если вы это сделаете, Django на самом деле разрезает QuerySet до того, как запрос будет выполнен, поэтому SQL-запрос будет использовать OFFSET и LIMIT, чтобы получить только записи, которые вы фактически увидите. Но вы не можете этого сделать, если вы не сможете вкратце поиска в один запрос каким-то образом.
Учитывая, что у всех трех моделей есть поля заголовка и тела, почему бы не использовать наследование модели? Просто у всех трех моделей наследуется от общего предка, который имеет название и тело, и выполняет поиск как один запрос на модели предка.
Ответ 6
Если вы хотите связать множество запросов, попробуйте следующее:
from itertools import chain
result = list(chain(*docs))
где: docs - это список запросов
Ответ 7
DATE_FIELD_MAPPING = {
Model1: 'date',
Model2: 'pubdate',
}
def my_key_func(obj):
return getattr(obj, DATE_FIELD_MAPPING[type(obj)])
And then sorted(chain(Model1.objects.all(), Model2.objects.all()), key=my_key_func)
Цитата из https://groups.google.com/forum/#!topic/django-users/6wUNuJa4jVw. См. Алекс Гейнор
Ответ 8
Похоже, t_rybik создал комплексное решение на http://www.djangosnippets.org/snippets/1933/
Ответ 9
Для поиска лучше использовать специальные решения, такие как Haystack - он очень гибкий.
Ответ 10
вот идея... просто вытащить одну полную страницу результатов из каждого из трех, а затем выбросить 20 наименее полезных... это устраняет большие запросы и таким образом вы жертвуете только небольшим исполнением, а не много
Ответ 11
Требования: Django==2.0.2
, django-querysetsequence==0.8
Если вы хотите объединить querysets
и по-прежнему использовать QuerySet
, вы можете проверить django-queryset-sequence.
Но одна заметка об этом. Он принимает только два querysets
качестве аргумента. Но с помощью Python reduce
вы всегда можете применить его к нескольким queryset
.
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})
Ответ 12
Это может быть достигнуто двумя способами.
1-й способ сделать это
Используйте оператор объединения для 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 # this would take union of two querysets
2-й способ сделать это
Еще один способ выполнения операции объединения двух наборов запросов - использование цепной функции itertools.
from itertools import chain
combined_results = list(chain(pagelist1, pagelist2))
Ответ 13
Эта рекурсивная функция объединяет массив наборов запросов в один набор запросов.
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