Как объединить 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?

18 ответов

Решение

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

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

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

matches = pages | articles | posts

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

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

Связанные, для смешивания наборов запросов из той же модели или для похожих полей из нескольких моделей, начиная с 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, даже если аргументы являются QuerySets других моделей. Передача разных моделей работает до тех пор, пока список SELECT одинаков во всех QuerySets (по крайней мере, типы, имена не имеют значения, если типы в одном и том же порядке).

Кроме того, только LIMIT, OFFSET и ORDER BY (то есть нарезка и order_by()) разрешены в результирующем QuerySet. Кроме того, базы данных накладывают ограничения на то, какие операции разрешены в комбинированных запросах. Например, большинство баз данных не допускают LIMIT или OFFSET в комбинированных запросах.

https://docs.djangoproject.com/en/1.11/ref/models/querysets/

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

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

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

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

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

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

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

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

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

Вы можете использовать Union

qs = qs1.union(qs2, qs3)

Но если вы хотите подать заявку order_byна сторонних моделях объединенного набора запросов.. тогда вам нужно выбрать их заранее таким образом.. иначе это не сработает

пример

qs = qs1.union(qs2.select_related("foreignModel"), qs3.select_related("foreignModel"))
qs.order_by("foreignModel__prop1")

где prop1 это недвижимость по иностранному образцу

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/. Смотри Алекс Гейнор

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

Если вы хотите объединить querysets и до сих пор выйти с QuerySetВы можете проверить https://github.com/percipient/django-querysetsequence.

Но одно замечание по этому поводу. Это займет всего два querysets как аргумент Но с питоном 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})

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

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

Лучше всего использовать встроенные методы Django:

      # Union method
result_list = page_list.union(article_list, post_list)

Это вернет объединение всех объектов в этих наборах запросов.

Если вы хотите получить только те объекты, которые входят в три набора запросов, вам понравится встроенный метод наборов запросов, intersection.

      # intersection method
result_list = page_list.intersection(article_list, post_list)

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

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

result_list = list(page_list) + list(article_list) + list(post_list)

Вы можете использовать «|» (побитовое или), чтобы объединить наборы запросов одной и той же модели, как показано ниже:

      # "store/views.py"

from .models import Food
from django.http import HttpResponse
                                                
def test(request):
                                             # ↓ Bitwise or
    result = Food.objects.filter(name='Apple') | Food.objects.filter(name='Orange')
    print(result)
    return HttpResponse("Test")

Вывод на консоль:


И вы можете использовать|=чтобы добавить набор запросов той же модели, как показано ниже:

      # "store/views.py"

from .models import Food
from django.http import HttpResponse
                                                
def test(request):
    result = Food.objects.filter(name='Apple')
         # ↓↓ Here
    result |= Food.objects.filter(name='Orange')
    print(result)
    return HttpResponse("Test")

Вывод на консоль:

      <QuerySet [<Food: Apple>, <Food: Orange>]>
[22/Jan/2023 12:51:44] "GET /store/test/ HTTP/1.1" 200 9

Будьте осторожны при добавлении набора запросов другой модели, как показано ниже:

      # "store/views.py"

from .models import Food, Drink
from django.http import HttpResponse
                                                
def test(request):
          # "Food" model                      # "Drink" model
    result = Food.objects.filter(name='Apple') | Drink.objects.filter(name='Milk')
    print(result)
    return HttpResponse("Test")

Ниже есть ошибка:

      AssertionError: Cannot combine queries on two different base models.
[22/Jan/2023 13:40:54] "GET /store/test/ HTTP/1.1" 500 96025

Но если добавить пустой набор запросов другой модели, как показано ниже:

      # "store/views.py"

from .models import Food, Drink
from django.http import HttpResponse
                                                
def test(request):
          # "Food" model                       # Empty queryset of "Drink" model 
    result = Food.objects.filter(name='Apple') | Drink.objects.none()
    print(result)
    return HttpResponse("Test")

Ниже ошибки нет:

      <QuerySet [<Food: Apple>]>
[22/Jan/2023 13:51:09] "GET /store/test/ HTTP/1.1" 200 9

Снова будьте осторожны, добавляя объект с помощью get() , как показано ниже:

      # "store/views.py"

from .models import Food
from django.http import HttpResponse
                                                
def test(request):
    result = Food.objects.filter(name='Apple')
                         # ↓↓ Object
    result |= Food.objects.get(name='Orange')
    print(result)
    return HttpResponse("Test")

Ниже есть ошибка:

      AttributeError: 'Food' object has no attribute '_known_related_objects'
[22/Jan/2023 13:55:57] "GET /store/test/ HTTP/1.1" 500 95748

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

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

Чтобы получить пересечение обоих наборов запросов:

      result = first_queryset.intersection(second_queryset)
Другие вопросы по тегам