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

StackOverflow https://stackoverflow.com/questions/431628

Вопрос

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

Это было полезно?

Решение

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

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

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

Теперь можно отсортировать полученный список, например, по дате (в соответствии с просьбой в комментарии Хасена к другому ответу). Функция 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'))

Другие советы

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

matches = pages | articles | posts

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

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

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

union()

union(*other_qs, all=False)

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

>>> qs1.union(qs2, qs3)

Оператор ОБЪЕДИНЕНИЯ по умолчанию выбирает только различные значения.Чтобы разрешить дублирование значений, используйте аргумент all=True .

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

Кроме того, только ОГРАНИЧЕНИЕ, СМЕЩЕНИЕ и ПОРЯДОК ПО (т.е.нарезка и order_by()) разрешены в результирующем наборе запросов.Далее, базы данных накладывают ограничения на то, какие операции разрешены в объединенных запросах. Например, большинство баз данных не допускают ОГРАНИЧЕНИЯ или СМЕЩЕНИЯ в объединенных запросах.

https://docs.djangoproject.com/en/1.11/ref/models/querysets/#django.db.models .запрос.Набор запросов.объединение

Вы можете использовать класс 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)

Затем используйте совпадения со страницей, как вы использовали result_list в своем примере.

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

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

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

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

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

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

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

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 -Пользователи / 6wUNuJa4jVw . Смотрите Алекс Гейнор

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

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

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

Но одна заметка об этом. В качестве аргумента требуется только два querysets . Но с помощью Python redu вы всегда можете применить его к нескольким queryset s.

from functools import reduce
from queryset_sequence import QuerySetSequence

combined_queryset = reduce(QuerySetSequence, list_of_queryset)

И это все. Ниже приведена ситуация, с которой я столкнулся, и то, как я применил понимание списка , 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-й способ сделать это

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

Для экземпляра

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))

Эта рекурсивная функция объединяет массив наборов запросов в один набор запросов.

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
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top