我试图建立一个我正在建立的Django网站的搜索,在这个搜索中,我用三种不同的模式进行搜索。为了在搜索结果列表上进行分页,我想使用一个通用的object_list视图来显示结果。但要做到这一点,我必须将三个查询集合并为一个。

我该怎么做?我已经试过了:

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

但这行不通。当我尝试在通用视图中使用该列表时,会出现错误。列表缺少克隆属性。

如何合并page_list、article_list和post_list这三个列表?


当前方法的一个大缺点是它对大型搜索结果集的效率低下,因为每次都必须从数据库中删除整个结果集,即使您只打算显示一页结果。

为了只从数据库中提取实际需要的对象,必须对QuerySet而不是列表使用分页。如果这样做,Django实际上会在执行查询之前对QuerySet进行切片,因此SQL查询将使用OFFSET和LIMIT来只获取实际显示的记录。但是,除非您能够以某种方式将搜索塞进一个查询中,否则无法做到这一点。

既然您的三个模型都有标题和正文字段,为什么不使用模型继承?只需让所有三个模型都继承自具有标题和正文的共同祖先,并作为对祖先模型的单个查询执行搜索。


您可以使用下面的QuerySetChain类。当它与Django的分页器一起使用时,它应该只对所有查询集进行COUNT(*)查询,而只对记录显示在当前页面上的查询集进行SELECT()查询。

注意,如果使用带有泛型视图的QuerySetChain,则需要指定template_name=,即使链接的查询集都使用相同的模型。

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模块是在Python2.3中引入的,因此它应该可以在Django运行的所有Python版本中使用。


将查询集连接到列表中是最简单的方法。如果所有查询集都会命中数据库(例如,因为结果需要排序),这不会增加更多成本。

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而不是lambda。我记得读过关于它更快的文章,但我没有看到一百万个项目列表有明显的速度差异。

from operator import attrgetter
result_list = sorted(
    chain(page_list, article_list, post_list),
    key=attrgetter('date_created'))

这里有一个想法。。。只需从三个人中的每一个人中抽出一整页的结果,然后扔掉20个最不有用的结果。。。这消除了大型查询集,这样只会牺牲一点性能,而不会牺牲很多性能。


试试看:

matches = pages | articles | posts

它保留了查询集的所有功能,如果您想按或类似方式排序,这是很好的。

请注意:这不适用于来自两个不同模型的查询集。


如果要链接大量查询集,请尝试以下操作:

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/#!主题/django用户/6wUNuJa4jVw。见Alex Gaynor


相关的,对于混合来自同一模型的查询集,或者对于来自几个模型的类似字段,从Django 1.11开始,也可以使用QuerySet.union()方法:

union()联合(*other_qs,all=False)Django 1.11新增。使用SQL的UNION运算符组合两个或多个QuerySet的结果。例如:>>>qs1.联合(qs2,qs3)默认情况下,UNION运算符仅选择不同的值。要允许重复值,请使用all=True论点union()、intersection()和difference()返回模型实例第一个QuerySet的类型,即使参数是其他型号。只要SELECT列表在所有QuerySet中都是相同的(至少是类型,名称不是只要类型的顺序相同)。此外,只有LIMIT、OFFSET和ORDER BY(即切片和order_by())。此外,数据库对允许在联合查询。例如,大多数数据库不允许LIMIT或OFFSET组合查询。


要求:Django==2.0.2,Django querysetsequence==0.8

如果您希望组合查询集,但仍然使用QuerySet,则可能需要检查django查询集序列。

但需要注意的是,它只需要两个查询集作为参数。但使用python-reduce,您可以始终将其应用于多个查询集。

from functools import reduce
from queryset_sequence import QuerySetSequence

combined_queryset = reduce(QuerySetSequence, list_of_queryset)

下面是我遇到的一个情况,以及我如何使用列表理解、reduce和django查询集序列

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

此递归函数将查询集数组连接成一个查询集。

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

这也可以通过两种方式实现。

第一种方法

对queryset|使用union运算符可以合并两个queryset。如果两个查询集属于同一模型/单个模型,则可以使用联合运算符组合查询集。

对于一个实例

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

第二种方法

实现两个查询集之间的组合操作的另一种方法是使用itertools链函数。

from itertools import chain
combined_results = list(chain(pagelist1, pagelist2))

这将在不使用任何其他库的情况下完成工作:

result_list = page_list | article_list | post_list

您可以使用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是外国模型中的属性。


最好的选择是使用Django内置方法:

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

这将返回这些查询集中所有对象的并集。

如果您只想获取三个查询集中的对象,您会喜欢内置的查询集方法intersection。

# intersection method
result_list = page_list.intersection(article_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")

控制台上的输出:

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