Как объединить 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. См. Алекс Гейнор

Ответ 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