Django ORM 合并查询集
在用python或者django写一些小工具应用的时候,有可能会遇到合并多个list到一个 list 的情况。单纯从技术角度来说,处理起来没什么难度,能想到的办法很多,但我觉得有一个很简单而且效率比较高的方法是我以前没注意到的。那就是利用 chain 方法来合并多个list. 同样也可以用来合并django 的 QuerySet.
1. 用 | 合并多个 QuerySet
1 |
|
因为 Django ORM 存在惰性查询机制,所以
articles = articles1 | articles2
并未真正执行SQL,所以在真正查询时会对两个查询的语句进行合并, 导致sql结果排序和预期不同。
2. 在Django 总用 chain 合并多个QuerySet
chain 是用C实现的,自然性能上比较可靠。
如果在Django中如果要合并同一个model的多个 QuerySet 的话,是可以采用这种方式的.
用chain 来实现会更方便,也没那么多限制条件,即使是不同的MODEL中查询出来的数据,都可以很方便的合并到一个 list 中去.
1 |
|
但是合并的结果是一个list,不是queryset。
而且这种方法需要额外遍历两个 QuerySet,而且排序在 Python 层面进行,会损失一些性能。在对查询到的数据进行操作时的一个重要原则是尽可能在最底层完成操作。例如尽量在数据库层面进行数值计算或者排序等操作,数据库无法完成操作时再上升到 Python 层面。那么使用 Django 的 ORM 有没有办法同时查询出多个模型的数据并对其进行计算或者排序呢?答案是使用查询集的 union
方法。
3. QuerySet 的 union 方法
union
方法其实对应数据库的 UNION
操作。
Post
模型,用于记录普通类型的博客文章, Material
模型,用于记录教程类文章。现在有一个需求,需要查询出全部的 Post
和 Material
,并以文章发表时间 pub_date
逆序排序(但置顶的普通类型文章必须排在最前面)用于博客首页文章列表展示。2 个模型定义分别定义如下:
1 |
|
def get_index_entry_queryset():
post_qs = Post.objects.all().order_by().annotate(
type=Value(‘p’, output_field=CharField(max_length=1)),
entry_pinned=F(‘pinned’))
post_qs = post_qs.values_list(
‘title’,’pub_date’,’entry_pinned’,’type’
)
material_qs = Material.objects.all().order_by().annotate(
type=Value('m', output_field=CharField(max_length=1)),
entry_pinned=Value(False, BooleanField()))
material_qs = material_qs.values_list(
'title','pub_date','entry_pinned','type'
)
entry_qs = post_qs.union(material_qs)
entry_qs = entry_qs.order_by('-entry_pinned', '-pub_date')
return entry_qs
1 |
|
def queryset_2_union_offset_limit(queryset_1, queryset_2, offset, limit):
“””
QuerySet union
:param queryset_1:
:param queryset_2:
:param offset:
:param limit:
:return:
“””
count_1 = queryset_1.count()
count_need_return = offset + limit
if offset + limit <= count_1:
return queryset_1[offset:count_need_return]
if offset < count_1:
ret = []
for i in queryset_1[offset:count_need_return]:
ret.append(i)
len_ret = len(ret)
if len_ret < count_need_return:
for i in queryset_2[:count_need_return - len_ret]:
ret.append(i)
return ret
if offset >= count_1:
return queryset_2[offset - count_1: count_need_return - count_1]
#### 注意事项
显然,要将两个不同模型的查询集合并为一个查询集,会有一些限制条件,因为涉及数据库的 `UNION` 操作,至少要保证两个模型查询出来的字段和类型都匹配。下面是 Django 的官方文档给出的 `union` 方法使用限制。
> * select 的字段类型必须匹配(字段名可以不同,但排列顺序要一致)。例如 field1 和 field 2 都是整数类型,select field1 和 select field 可以进行 union 操作,当引用时,以第一个 QuerySet 中的字段名进行引用。
> * 组合后的查询集,很多方法将不可用。
#### 总结
查询集的 `union` 方法可以将不同模型查询结果合并为一个查询集(使用数据库的 `UNION` 操作),这样可以将两条查询语句合并为一条,减少数据库的查询次数,同时还能在数据库层面对组合的数据进行排序等操作。但使用时要注意:
1. select 的字段类型必须匹配(字段名可以不同,但排列顺序要一致)
2. 确保 `annotate` 方法设置的查询字段顺序一致
3. 合并后的查询集,很多方法将不可用
4. 待合并的查询集不能有排序操作
5. 合并后的查询集不能对 `annotate` 设置的字段使用 F 表达式
6. 合并后的查询集排序时不能指定 null 的顺序