如何在Django视图中合并两个或多个查询集?


653

我正在尝试为正在构建的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")

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

有谁知道我可以合并三个列表,page_listarticle_listpost_list



对于搜索,最好使用Haystack之类的专用解决方案-它非常灵活。
minder 2010年

1
Django用户1.11和abv,请参见此答案-stackoverflow.com/a/42186970/6003362
Sahil Agarwal,

注意:这个问题仅限于极少数情况,当将3个不同的模型合并在一起后,您无需再次在清单中提取模型来区分类型数据。在大多数情况下-如果可以预期与众不同-接口将会错误。对于相同的模型:请参阅有关的答案union
斯瓦沃米尔Lenart

Answers:


1058

将查询集连接到列表是最简单的方法。如果无论如何将对所有查询集命中数据库(例如,由于需要对结果进行排序),则不会增加成本。

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

使用itertools.chainitertools在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'))

13
如果合并来自同一表的查询集以执行OR查询,并且具有重复的行,则可以使用groupby函数消除它们: from itertools import groupby unique_results = [rows.next() for (key, rows) in groupby(result_list, key=lambda obj: obj.id)]
Josh Russo

1
好的,所以在这种情况下有关groupby函数的nm。使用Q函数,您应该能够执行所需的任何OR查询:https
Josh Russo

2
@apelliciari Chain使用的内存明显少于list.extend,因为它不需要将两个列表都完全加载到内存中。
丹·盖尔

2
@AWrightIV这是该链接的新版本:docs.djangoproject.com/en/1.8/topics/db/queries/…–
Josh Russo

1
尝试这种方法,但是有'list' object has no attribute 'complex_filter'
Grillazz '16

466

尝试这个:

matches = pages | articles | posts

它保留了查询集的所有功能,如果您愿意order_by或类似的话,这很好。

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


10
但是,不适用于切片查询集。还是我错过了什么?
sthzg 2014年

1
我以前使用“ |”加入查询集 但并非总是能正常工作。最好使用“ Q”:docs.djangoproject.com/en/dev/topics/db/queries/…–
IgnacioPérez

1
它似乎没有使用Django 1.6创建重复项。
Teekin 2014年

15
|是集合并运算符,不是按位或。
e100 2015年

6
@ e100不,它不是set联合运算符。django重载按位“或”运算符:github.com/django/django/blob/master/django/db/models/…–
shangxiao

109

相关的,从Django 1.11开始,为了混合来自相同模型的查询集或来自几个模型的相似字段,还提供了一种qs.union()方法

union()

union(*other_qs, all=False)

Django 1.11中的新增功能。使用SQL的UNION运算符组合两个或多个QuerySet的结果。例如:

>>> qs1.union(qs2, qs3)

默认情况下,UNION运算符仅选择不同的值。要允许重复值,请使用all = True参数。

union(),intersection()和difference()返回第一个QuerySet类型的模型实例,即使参数是其他模型的QuerySet也是如此。只要所有QuerySet中的SELECT列表相同,传递不同的模型就可以工作(至少类型,名称只要类型相同就没有关系)。

另外,在结果QuerySet上仅允许LIMIT,OFFSET和ORDER BY(即slicing和order_by())。此外,数据库对组合查询中允许的操作设置了限制。例如,大多数数据库在组合查询中不允许LIMIT或OFFSET。

https://docs.djangoproject.com/zh-CN/1.11/ref/models/querysets/#django.db.models.query.QuerySet.union


对于需要唯一值的问题集,这是一个更好的解决方案。
燃烧的晶体

不适用于geodjango几何。
MarMat

你从哪里进口工会?它是否必须来自X个查询集之一?
杰克

是的,它是queryset的一种方法。
Udi

我认为它删除了搜索过滤器
Pierre Cordier

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)

然后matchesresult_list在示例中一样使用分页器。

itertools模块是在Python 2.3中引入的,因此在运行Django的所有Python版本中都应该可用。


5
好的方法,但是我在这里看到的一个问题是查询集被附加到“头到尾”。如果每个查询集按日期排序,而又需要组合集也按日期排序怎么办?
hasen

这肯定看起来很有希望,太好了,我必须尝试一下,但是我今天没有时间。如果能解决我的问题,我会尽快与您联系。做得好。
espenhogbakk,2009年

好的,我今天必须尝试,但是它没有用,首先它抱怨它不必具有_clone属性,所以我添加了一个,只是复制了_all并起作用,但是分页器似乎对此查询集有问题。我收到此分页器错误:“ len()的大小不正确的对象”
espenhogbakk,2009年

1
@Espen Python库:pdb,正在记录。外部:IPython,ipdb,django-logging,django-debug-toolbar,django-command-extensions,werkzeug。在代码中使用打印语句或使用日志记录模块。最重要的是,学会内省外壳。Google提供有关调试Django的博客文章。乐意效劳!
akaihola

4
@patrick,请参见djangosnippets.org/snippets/1103djangosnippets.org/snippets/1933 –尤其是后者是一个非常全面的解决方案
akaihola 2011年

27

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

为了仅从数据库中拉出您实际需要的对象,必须在QuerySet而不是列表上使用分页。如果执行此操作,则Django实际上会在执行查询之前对QuerySet进行切片,因此SQL查询将使用OFFSET和LIMIT仅获取您实际显示的记录。但是,除非您可以以某种方式将搜索塞入单个查询中,否则您将无法执行此操作。

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


23

如果您要链接很多查询集,请尝试以下操作:

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

其中:docs是查询集列表



8

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

第一种方法

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

对于一个实例

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

7

要求: Django==2.0.2django-querysetsequence==0.8

如果您想结合querysets使用a仍然很不错QuerySet,则可能需要签出django-queryset-sequence

但是有一点需要注意。仅需两个querysets参数即可。但是,使用python,reduce您可以始终将其应用于多个queryset

from functools import reduce
from queryset_sequence import QuerySetSequence

combined_queryset = reduce(QuerySetSequence, list_of_queryset)

就是这样。以下是我遇到的情况以及我的工作方式list comprehensionreduce以及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
难道Book.objects.filter(owner__mentor=mentor)不能做同样的事情?我不确定这是否是有效的用例。我认为在开始执行类似操作之前,Book可能需要有多个owner
威尔小号

是的,它做同样的事情。我尝试过这个。无论如何,也许这在其他情况下可能有用。感谢您指出了这一点。作为初学者,您并不是一开始就知道所有快捷方式。有时,您必须沿着蜿蜒的道路欣赏乌鸦的飞翔
chidimo

6

这是一个主意...只需从三个结果中拉出一整页的结果,然后丢弃20个最不有用的结果...这样就消除了大型查询集,因此您只牺牲了一点性能而不是很多



-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
我真的迷路了。
lycuid

我们将查询结果组合起来,使其不能在运行时使用,这样做实在是一个坏主意。因为有时它会在结果上添加重复项。
Devang Hingu
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.