如何在Django中合并多个QuerySet?

829

我正在尝试为我正在构建的Django网站构建搜索功能,在该搜索中,我要跨越三个不同的模型进行搜索。为了在搜索结果列表上获得分页,我想使用通用的object_list视图来显示结果。但是为了做到这一点,我必须将三个QuerySet合并成一个。

我该怎么做?我已经尝试过这样做:

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


1
看起来t_rybik在http://www.djangosnippets.org/snippets/1933/创建了一个全面的解决方案。 - akaihola
1
对于搜索,最好使用专门的解决方案,如Haystack - 它非常灵活。 - minder
1
Django用户1.11及以上版本,请参见此答案-https://dev59.com/03RC5IYBdhLWcg3wCMg6#42186970 - Sahil Agarwal
注意: 此问题仅限于极少数情况,即在合并3个不同的模型之后,您不需要再次提取模型以区分类型数据。对于大多数情况-如果需要区分-它将是错误的接口。对于相同的模型:请参阅有关“联合”的答案。 - Sławomir Lenart
16个回答

1232
将查询集合连接成一个列表是最简单的方法。如果数据库无论如何都会被查询集中的所有内容命中(例如因为结果需要排序),这不会增加额外的开销。
from itertools import chain
result_list = list(chain(page_list, article_list, post_list))

使用 itertools.chain 比逐一循环每个列表并添加元素更快,因为 itertools 是用 C 实现的。在连接之前将每个 queryset 转换为列表消耗的内存也较少。

现在可以按日期对结果列表进行排序(如 hasen j 对另一个答案的评论中所请求的)。sorted() 函数方便地接受生成器并返回一个列表:

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

您可以反转排序顺序:

result_list = sorted(
    chain(page_list, article_list, post_list),
    key=attrgetter('date_created'),
    reverse=True,
)

attrgetter 相当于以下 lambda(这是在 Python 2.4 之前必须执行的方式):

result_list = sorted(
    chain(page_list, article_list, post_list),
    key=lambda instance: instance.date_created,
)

20
如果合并来自同一张表的查询集以执行OR查询,并且有重复的行,则可以使用groupby函数消除它们: from itertools import groupby unique_results = [rows.next() for (key, rows) in groupby(result_list, key=lambda obj: obj.id)] - Josh Russo
2
好的,在这种情况下,关于groupby函数就不用考虑了。使用Q函数,您应该能够执行任何需要的OR查询:https://docs.djangoproject.com/en/1.3/topics/db/queries/#complex-lookups-with-q-objects - Josh Russo
3
@apelliciari Chain 使用的内存比list.extend少得多,因为它不需要将两个列表全部加载到内存中。 - Dan Gayle
2
@AWrightIV 这是该链接的新版本:https://docs.djangoproject.com/en/1.8/topics/db/queries/#complex-lookups-with-q-objects - Josh Russo
1
@akaihola 是的,我也想到了。那么...我在想如何以一种实际可行的方式,结合按日期排序的查询集进行分页处理...似乎需要一个Paginator功能请求。或者你的QuerySetChain答案 https://djangosnippets.org/snippets/1933/ - Purrell
显示剩余15条评论

582

试试这个:

matches = pages | articles | posts

它保留了查询集的所有函数,这很好,如果您想使用order_by或类似函数。

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


14
不过在切片查询集上无法工作,对吗?还是我漏了什么? - sthzg
2
我曾经使用 "|" 合并查询集,但并不总是有效。更好的方法是使用 "Q": https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups-with-q - Ignacio Pérez
25
这里的 | 是集合并运算符,而不是按位或。 - e100
15
不,它不是集合并运算符。Django重载了按位或运算符:https://github.com/django/django/blob/master/django/db/models/query.py#L308 - shangxiao
7
请注意,此解决方案不会保留顺序,因此集合{x,y,x}和集合{a,b,c}可能最终都变成{a,b,c,x,y,z},无论您使用s1 | s2还是s2 | s1,这使得|在许多情况下有点无用。 - Mike 'Pomax' Kamermans
显示剩余8条评论

167

相关的,对于混合来自同一模型的查询集,或者来自几个模型的类似字段,在Django 1.11开始还提供了一个QuerySet.union()方法:

union()

union(*other_qs, all=False)

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

>>> qs1.union(qs2, qs3)

UNION 操作符默认仅选择不同的值。要允许重复值,请使用 all=True 参数。

即使参数是其他模型的 QuerySet,union()、intersection() 和 difference() 也会返回第一个 QuerySet 类型的模型实例。只要所有 QuerySets 的 SELECT 列表相同(至少类型相同,名称无关紧要,只要类型按照相同顺序排列),传递不同的模型就可以正常工作。

此外,结果 QuerySet 上仅允许使用 LIMIT、OFFSET 和 ORDER BY(即切片和 order_by())。此外,数据库对组合查询中允许的操作有限制。例如,大多数数据库不允许在组合查询中使用 LIMIT 或 OFFSET。


1
这是一个更好的解决方案,适用于我的问题集,需要具有唯一值。 - Burning Crystals
1
你从哪里导入union呢?它必须来自X个查询集中的一个吗? - Jack
1
是的,它是一个queryset的方法。 - Udi
我认为它会删除搜索过滤器。 - Pierre Cordier
13
记住,在使用union()后,您将无法再次对这个查询集使用filter()方法进行过滤。filter()方法会默默地失败。至少在Django 2.2版本中是如此。 - Qback
在使用union时,请小心在管理站点中的应用:https://dev59.com/hLvpa4cB1Zd3GeqPB_mL - djvg

83

您可以使用下面的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)

然后使用matches与分页器一起使用,就像您在示例中使用result_list一样。

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


7
好的,我会尽力进行翻译。以下是需要翻译的内容:Nice approach, but one problem I see here is that the query sets are appended "head-to-tail". What if each queryset is ordered by date and one needs the combined-set to also be ordered by date?翻译:方法不错,但我发现这里有一个问题,即查询集被“头对尾”地附加在一起。如果每个查询集都按日期排序,并且需要将组合集也按日期排序怎么办? - hasen
@Espen 从Django的8121版本开始,分页器似乎总是首先尝试调用count()方法。如果你的Django版本较旧,请尝试将count()重命名为__len__()。 - akaihola
@Espen 我在类中添加了一个适当的 _clone() 方法。如果指定 template_name=,它现在应该可以与 object_list 通用视图一起使用了。 - akaihola
1
@Espen Python库:pdb,logging。外部:IPython,ipdb,django-logging,django-debug-toolbar,django-command-extensions,werkzeug。在代码中使用print语句或使用logging模块。最重要的是,在shell中学习内省。谷歌搜索有关调试Django的博客文章。很高兴能帮忙! - akaihola
4
请参见 http://djangosnippets.org/snippets/1103/ 和 http://djangosnippets.org/snippets/1933/。尤其是后者是一个非常全面的解决方案。 - akaihola
显示剩余8条评论

43

如果你想要连接多个查询集,请尝试这样做:

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

其中:docs是一个查询集列表


如何对这些结果进行排序?我尝试过 results.reverse() 但不起作用。 - parmer_110

32

你当前的方法存在一个严重问题,当搜索结果集很大时,它的效率非常低下,因为每次需要从数据库中拉取整个结果集,即使你只想显示一页结果。

为了仅拉取实际需要的对象,你必须在QuerySet上使用分页,而不是列表。如果这样做,Django实际上会在执行查询之前对QuerySet进行切片,因此SQL查询将使用OFFSET和LIMIT仅获取你实际要显示的记录。但是,除非你能将搜索压缩成单个查询,否则无法这样做。

考虑到你的所有三个模型都有标题和正文字段,为什么不使用模型继承?只需让所有三个模型从共同祖先继承标题和正文,并在祖先模型上执行搜索作为单个查询。


30

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

第一种方法

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

例如

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

第二种方法

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

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

4
可以使用functools.reduce(operator.or_, [pagelist1, pagelist2])代替itertools.chain(它会逐个运行每个查询),以编程方式应用您的第一种方法。这将产生一个单一的查询。 - Cornflex

21

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


16

9

需求: Django==2.0.2, django-querysetsequence==0.8

如果你想要合并querysets并且仍然希望得到一个QuerySet,那么你可能需要查看django-queryset-sequence

但是需要注意的是:它只接受两个querysets作为参数。不过,通过使用Python的reduce函数,你可以将其应用于多个querysets

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

2
Book.objects.filter(owner__mentor=mentor) 不是做同样的事情吗?我不确定这是否是一个有效的用例。我认为一个 Book 可能需要有多个 owner,才需要开始做这样的事情。 - Will S
1
是的,它做了同样的事情。我试过了。无论如何,也许在其他情况下这可能会有用。感谢您指出这一点。作为初学者,您并不完全知道所有的快捷方式。有时候你得走弯路才能欣赏到直线飞行的美妙。 - chidimo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接