Как объединить 2 или более наборов запросов в представлении Django?

563

Я пытаюсь построить поиск для сайта 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?

  • 0
    Похоже, t_rybik создал комплексное решение на djangosnippets.org/snippets/1933
  • 0
    Для поиска лучше использовать специальные решения, такие как Haystack - это очень гибко.
Показать ещё 1 комментарий
Теги:
search
django-queryset
django-q

12 ответов

892
Лучший ответ

Объединение запросов в список является самым простым подходом. Если база данных будет удалена для всех запросов в любом случае (например, потому что результат нужно сортировать), это не добавит дополнительных затрат.

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'))
  • 88
    Привод голосованием: нахождение этого поста оказалось чрезвычайно полезным для меня сегодня. Спасибо!
  • 11
    Если вы объединяете наборы запросов из одной таблицы для выполнения запроса ИЛИ и имеете дублированные строки, вы можете удалить их с помощью функции groupby: from itertools import groupby unique_results = [rows.next() for (key, rows) in groupby(result_list, key=lambda obj: obj.id)]
Показать ещё 17 комментариев
386

Попробуйте следующее:

matches = pages | articles | posts

Сохраняет все функции запросов, которые хороши, если вы хотите order_by или подобное.

К сожалению, это не работает с запросами из двух разных моделей...

  • 9
    Однако не работает на нарезанных наборах запросов. Или я что-то упустил?
  • 0
    | кажется больше похоже на list +, чем set |, т. е. может создавать дубликаты.
Показать ещё 10 комментариев
86

Связанный, для смешивания запросов с той же модели или для похожих полей из нескольких моделей, Начиная с 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

  • 0
    Это лучшее решение для моей задачи, которая должна иметь уникальные значения.
  • 0
    Не работает для геометрии геоджанго.
73

Вы можете использовать класс 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
    Хороший подход, но одна проблема, которую я вижу здесь, состоит в том, что наборы запросов добавляются «голова к хвосту». Что если каждый набор запросов упорядочен по дате, и нужно, чтобы объединенный набор также был упорядочен по дате?
  • 0
    Это, безусловно, выглядит многообещающе, здорово, мне придется это попробовать, но у меня сегодня нет времени. Я вернусь к вам, если это решит мою проблему. Отличная работа.
Показать ещё 11 комментариев
24

Большой недостаток вашего нынешнего подхода - его неэффективность с большими наборами результатов поиска, так как каждый раз приходится вытаскивать весь результирующий набор из базы данных, хотя вы только собираетесь отображать одну страницу результатов.

Чтобы только вытащить нужные вам объекты из базы данных, вам нужно использовать разбиение на страницы в QuerySet, а не на список. Если вы это сделаете, Django на самом деле разрезает QuerySet до того, как запрос будет выполнен, поэтому SQL-запрос будет использовать OFFSET и LIMIT, чтобы получить только записи, которые вы фактически увидите. Но вы не можете этого сделать, если вы не сможете вкратце поиска в один запрос каким-то образом.

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

18

Если вы хотите связать множество запросов, попробуйте следующее:

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

где: docs - это список запросов

13
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

Для поиска лучше использовать специальные решения, такие как Haystack - он очень гибкий.

8

Похоже, t_rybik создал комплексное решение на http://www.djangosnippets.org/snippets/1933/

6

Требования: 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})
  • 1
    Разве Book.objects.filter(owner__mentor=mentor) не делает то же самое? Я не уверен, что это правильный вариант использования. Я думаю, что для Book может потребоваться несколько owner прежде чем вы начнете делать что-то подобное.
  • 0
    Да, это делает то же самое. Я попробовал это. Во всяком случае, возможно, это может быть полезно в какой-то другой ситуации. Спасибо что подметил это. Вы не совсем начинаете, зная все ярлыки, как начинающий. Иногда нужно путешествовать по извилистой дороге, чтобы оценить воронью муху
5

вот идея... просто вытащить одну полную страницу результатов из каждого из трех, а затем выбросить 20 наименее полезных... это устраняет большие запросы и таким образом вы жертвуете только небольшим исполнением, а не много

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню