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

我正在尝试为我正在构建的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_listarticle_listpost_list这三个列表?

420126 次浏览

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

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

假设您的所有三个模型都有title和body字段,为什么不使用模型继承呢?只需让所有三个模型都继承自具有title和body的共同祖先,并作为对祖先模型的单个查询执行搜索。

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

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

from itertools import islice, chain
class QuerySetChain(object):"""Chains multiple subquerysets (possibly of different models) and behaves asone queryset.  Supports minimal methods needed for use withdjango.core.paginator."""
def __init__(self, *subquerysets):self.querysets = subquerysets
def count(self):"""Performs a .count() for all subquerysets and returns the number ofrecords 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 allsubquerysets."""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版本中都可用。

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

from itertools import chainresult_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 attrgetterresult_list = sorted(chain(page_list, article_list, post_list),key=attrgetter('date_created'))

这里有一个想法……只需从三个结果中的每一个中拉出一整页结果,然后抛出20个最不有用的结果……这消除了大的查询集,这样你只牺牲了一点性能,而不是很多。

试试这个:

matches = pages | articles | posts

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

请注意:这对来自两个不同模型的查询集不起作用。

如果你想链接很多查询集,试试这个:

from itertools import chainresult = 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 a#0方法也可用:

union()

union(*other_qs, all=False)

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

>>> qs1.union(qs2, qs3)

UNION运算符默认仅选择不同的值。要允许重复值,请使用all=True论点。

联合()、交集()和差异()返回第一个QuerySet的类型,即使参数是QuerySets其他模型。传递不同的模型只要SELECT列表在所有QuerySet中都是相同的(至少类型是相同的,名称不是只要是相同顺序的类型)。

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

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

如果您想合并querysets并仍然输出QuerySet,您可能需要查看django-查询集-序列

但是关于它的一个注意事项。它只需要两个querysets作为参数。但是对于pythonreduce,您始终可以将其应用于多个queryset

from functools import reducefrom queryset_sequence import QuerySetSequence
combined_queryset = reduce(QuerySetSequence, list_of_queryset)

就是这样。下面是我遇到的情况以及我如何使用list comprehensionreducedjango-queryset-sequence

from functools import reducefrom django.shortcuts import renderfrom 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 menteesmentee_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]=tmpar.pop(1)return ar

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

第一种方法来做到这一点

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

对于一个实例

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

第二种方法来做到这一点

实现两个查询集之间合并操作的另一种方法是使用迭代工具链函数。

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

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

result_list = page_list | article_list | post_list

您可以使用联盟

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 methodresult_list = page_list.union(article_list, post_list)

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

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

# intersection methodresult_list = page_list.intersection(article_list, post_list)