关于搜索:如何在django视图中组合2个或更多的查询集?

How to combine 2 or more querysets in a Django view?

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

我该怎么做?我试过了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
2
from itertools import chain
result_list = list(chain(page_list, article_list, post_list))

由于itertools是在C语言中实现的,因此使用itertools.chain比循环每个列表并逐个附加元素要快,而且比在连接之前将每个查询集转换为列表消耗的内存也要少。

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

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

如果您使用的是python 2.4或更高版本,那么可以使用attrgetter而不是lambda。我记得我读到过关于它更快,但我没有看到一个明显的速度差异为一百万个项目清单。

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


试试这个:

1
matches = pages | articles | posts

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

哎呀,请注意,这对来自两个不同模型的查询集不起作用…


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

union()

1
union(*other_qs, all=False)

New in Django 1.11. Uses SQL’s UNION operator to combine the results of two or more QuerySets. For example:

1
>>> qs1.union(qs2, qs3)

The UNION operator selects only distinct values by default. To allow duplicate values, use the all=True
argument.

union(), intersection(), and difference() return model instances of
the type of the first QuerySet even if the arguments are QuerySets of
other models. Passing different models works as long as the SELECT
list is the same in all QuerySets (at least the types, the names don’t
matter as long as the types in the same order).

In addition, only LIMIT, OFFSET, and ORDER BY (i.e. slicing and
order_by()) are allowed on the resulting QuerySet. Further, databases
place restrictions on what operations are allowed in the combined
queries. For example, most databases don’t allow LIMIT or OFFSET in
the combined queries.

https://docs.djangoproject.com/en/1.11/ref/models/queryset/django.db.models.query.queryset.union


您可以使用下面的QuerySetChain类。当与django的paginator一起使用时,只应使用COUNT(*)查询所有查询集的数据库,只应使用SELECT()查询当前页面上显示记录的查询集。

注意,如果将QuerySetChain与通用视图一起使用,则需要指定template_name=,即使链接的查询集都使用相同的模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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()

在您的示例中,用法是:

1
2
3
4
5
6
7
8
9
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与paginator一起使用,就像在示例中使用result_list一样。

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


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

为了只从数据库中下拉实际需要的对象,必须对查询集而不是列表使用分页。如果这样做,Django实际上会在执行查询之前分割查询集,因此SQL查询将使用偏移量和限制来获取实际显示的记录。但是,除非您能以某种方式将搜索塞进一个查询中,否则无法执行此操作。

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


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

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

其中:docs是查询集列表


1
2
3
4
5
6
7
8
9
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


要求:江户十一〔13〕、江户十一〔14〕。

如果您想要合并querysets,并且仍然使用QuerySet,那么您可能需要检查django queryset序列。

但有一点值得注意。它只需要两个querysets作为论据。但是使用python reduce,您可以将其应用于多个QuerySet

1
2
3
4
from functools import reduce
from queryset_sequence import QuerySetSequence

combined_queryset = reduce(QuerySetSequence, list_of_queryset)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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})


有个主意……只需将三个结果中的每一个都下拉一整页,然后扔掉20个最不有用的结果…这样就消除了大量的查询集,这样只会牺牲一点性能,而不会牺牲很多性能。


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

第一种方法

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

举例来说

1
2
3
4
5
6
7
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链函数。

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

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

1
2
3
4
5
6
7
8
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