Как объединить 2 или более наборов запросов в представлении Django? - PullRequest
593 голосов
/ 10 января 2009

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

Ответы [ 11 ]

975 голосов
/ 12 января 2009

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

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'))
414 голосов
/ 28 апреля 2009

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

matches = pages | articles | posts

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

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

91 голосов
/ 12 февраля 2017

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

union()

union(*other_qs, all=False)

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

>>> qs1.union(qs2, qs3)

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

union (), intersection () и diff () возвращают экземпляры модели тип первого QuerySet, даже если аргументы QuerySet другие модели. Передача разных моделей работает до тех пор, пока 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

73 голосов
/ 11 января 2009

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

26 голосов
/ 11 января 2009

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

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

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

20 голосов
/ 27 ноября 2012

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

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

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

15 голосов
/ 23 декабря 2013
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. См. Алекс Гейнор

5 голосов
/ 12 февраля 2018

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

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

Но одна заметка об этом. Требуется только два querysets в качестве аргумента. Но с python reduce вы всегда можете применить его к нескольким queryset s.

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})
5 голосов
/ 13 января 2009

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

4 голосов
/ 07 апреля 2019

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

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