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

654

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

espenhogbakk
источник
Похоже, t_rybik создал комплексное решение на djangosnippets.org/snippets/1933
akaihola
Для поиска лучше использовать специальные решения, такие как Haystack - он очень гибкий.
моторист
1
Пользователи Django 1.11 и abv, посмотрите этот ответ - stackoverflow.com/a/42186970/6003362
Сахил Агарвал
примечание : вопрос ограничен очень редким случаем, когда после объединения трех разных моделей вам не нужно снова извлекать модели из списка, чтобы различать данные по типам. В большинстве случаев - если ожидается различие - это будет неправильный интерфейс. Для тех же моделей: см. Ответы о union.
Славомир Ленарт

Ответы:

1058

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

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

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

Теперь можно отсортировать итоговый список, например, по дате (как было запрошено в комментарии hasen 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'))
akaihola
источник
14
Если вы объединяете наборы запросов из одной таблицы для выполнения запроса ИЛИ и имеете дублированные строки, вы можете удалить их с помощью функции groupby: from itertools import groupby unique_results = [rows.next() for (key, rows) in groupby(result_list, key=lambda obj: obj.id)]
Джош Руссо,
1
Хорошо, так о нм о функции groupby в этом контексте. С помощью функции Q вы сможете выполнить любой запрос OR: https://docs.djangoproject.com/en/1.3/topics/db/queries/#complex-lookups-with-q-objects
Джош Руссо
2
@apelliciari Chain использует значительно меньше памяти, чем list.extend, поскольку нет необходимости полностью загружать оба списка в память.
Дэн Гейл
2
@AWrightIV Вот новая версия этой ссылки: docs.djangoproject.com/en/1.8/topics/db/queries/…
Джош Руссо
1
пробую этот подход, но имейте'list' object has no attribute 'complex_filter'
grillazz
466

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

matches = pages | articles | posts

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

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

Даниэль Холмс
источник
10
Не работает на нарезанных наборах запросов, все же. Или я что-то упустил?
sthzg
1
Я использовал, чтобы присоединиться к наборам запросов, используя "|" но не всегда работает нормально. Лучше использовать «Q»: docs.djangoproject.com/en/dev/topics/db/queries/…
Игнасио Перес
1
Кажется, он не создает дубликаты, используя Django 1.6.
Текин
15
Здесь |оператор объединения множеств, а не побитовое ИЛИ.
e100
6
@ e100 нет, это не оператор объединения множеств. django перегружает оператор побитового ИЛИ: github.com/django/django/blob/master/django/db/models/…
shangxiao
109

Связанные, для смешивания querysets из одной и той же модели, или для аналогичных полей из нескольких моделей, начиная с Django 1.11 qs.union()метод также доступен:

union()

union(*other_qs, all=False)

Новое в Джанго 1.11 . Использует оператор SQL UNION для объединения результатов двух или более QuerySets. Например:

>>> qs1.union(qs2, qs3)

Оператор UNION выбирает только отдельные значения по умолчанию. Чтобы разрешить повторяющиеся значения, используйте аргумент all = True.

union (), intersection () и diff () возвращают экземпляры модели типа первого QuerySet, даже если аргументы являются QuerySets других моделей. Передача разных моделей работает до тех пор, пока список SELECT одинаков во всех QuerySets (по крайней мере, типы, имена не имеют значения, если типы в одном и том же порядке).

Кроме того, только 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

Уди
источник
Это лучшее решение для моей задачи, которая должна иметь уникальные значения.
Горящие кристаллы
Не работает для геометрии геоджанго.
MarMat
Откуда вы импортируете союз? Должен ли он исходить из одного из X наборов запросов?
Джек,
Да, это метод queryset.
Уди
Я думаю, что это удаляет поисковые фильтры
Пьер Кордье
76

Вы можете использовать QuerySetChainкласс ниже. При использовании его с пагинатором 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с пагинатором, как вы использовали result_listв своем примере.

itertoolsМодуль был введен в Python 2.3, поэтому она должна быть доступна во всех версиях Python Django работает под управлением .

akaihola
источник
5
Хороший подход, но одна проблема, которую я вижу здесь, состоит в том, что наборы запросов добавляются «голова к хвосту». Что если каждый набор запросов упорядочен по дате, и нужно, чтобы объединенный набор также был упорядочен по дате?
Хасен
Это, безусловно, выглядит многообещающе, здорово, мне придется это попробовать, но у меня сегодня нет времени. Я вернусь к вам, если это решит мою проблему. Отличная работа.
espenhogbakk
Хорошо, мне пришлось попробовать сегодня, но это не сработало, сначала он пожаловался, что ему не нужно атрибута _clone, поэтому я добавил его, просто скопировал _all, и это сработало, но, похоже, у paginator есть некоторые проблемы с этим набором запросов. Я получаю сообщение об ошибке: «len () объекта без размера»
espenhogbakk
1
@Espen Python библиотека: pdb, ведение журнала. Внешние: IPython, ipdb, django-logging, django-debug-toolbar, django-command-extensions, werkzeug. Используйте операторы печати в коде или используйте модуль регистрации. Прежде всего, научиться самоанализу в оболочке. Google для сообщений в блоге об отладке Django. Рад помочь!
akaihola
4
@patrick, см. djangosnippets.org/snippets/1103 и djangosnippets.org/snippets/1933 - особенно последнее - очень всеобъемлющее решение
akaihola
27

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

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

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

Карл Мейер
источник
23

Если вы хотите связать много наборов запросов, попробуйте это:

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

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

vutran
источник
8

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

1-й способ сделать это

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

Для примера

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))
Деванг Падияр
источник
7

Требования: Django==2.0.2 ,django-querysetsequence==0.8

В случае, если вы хотите объединить querysetsи все еще выйти с QuerySet, вы можете проверить django-queryset-sequence .

Но одно замечание по этому поводу. Это только два, querysetsкак аргумент. Но с Python reduceвы всегда можете применить его к нескольким querysets.

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

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

Jiaaro
источник
1

Это сделает работу без использования каких-либо других библиотек

result_list = list(page_list) + list(article_list) + list(post_list)
Сатьям Фаждар
источник
-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
Петр Дворжачек
источник
1
Я буквально потерян.
жидкость
мы объединяем результат запроса, который нельзя использовать во время выполнения, и это действительно плохая идея. потому что иногда это добавляет дублирование над результатом.
Деванг Хингу