Django 开发中有时候会遇到这样的需求:查询到不同模型(Model) 的查询集(QuerySet),需要将其合并成一个查询集,甚至还希望能够对合并后的查询集排序,以便在模板中循环展示。

sorted
>>> import itertools
>>> qs1 = Post.objects.all()
>>> qs2 = Material.objects.all()
>>> qs = itertools.chain(qs1, sq2)
>>> sorted(qs, key=lambda o: o.created_time, reverse=True)
union

QuerySet 的 union 方法

unionUNIONPostMaterialPostMaterialpub_date
frpm django.db import models

class Post(modes.Model):
    title = models.CharField(max_length=255)
    body = models.TextField()
    pub_date = models.DateTimeField()
    pinned = models.BooleanField(default=False)

    class Meta:
        ordering = ['-pinned', '-pub_date']

class Material(models.Model):
    title = models.CharField(max_length=255)
    body = models.TextField()
    pub_date = models.DateTimeField()

    class Meta:
        ordering = ['-pub_date']
MaterialPostpinnedpinned
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
annotatetypeMaterialpinnedannotateentry_pinnedFalsePostpinnedannotatepinned
entry_qsquery
SELECT "blog_post"."title", "blog_post"."pub_date", 'p' AS "type", "blog_post"."pinned" AS "entry_pinned"
FROM "blog_post"
UNION
SELECT "courses_material"."title", "courses_material"."pub_date", 'm' AS "type", False AS "entry_pinned"
FROM "courses_material"
ORDER BY (4) DESC, (2) DESC

数据库查询结果如下:

title pub_date type entry_pinned
Markdown 测试 2019-09-23 15:35:47.898271 p 1
test 2019-09-15 13:13:00 p 0
分类、归档和标签页 2019-09-07 01:41:00 m 0
页面侧边栏:使用自定义模板标签 2019-08-29 23:49:00 m 0
UNIONunionall=TrueUNION ALL

注意事项

UNIONunion
  • select 的字段类型必须匹配(字段名可以不同,但排列顺序要一致)。例如 field1 和 field 2 都是整数类型,select field1 和 select field 可以进行 union 操作,当引用时,以第一个 QuerySet 中的字段名进行引用。
  • 组合后的查询集,很多方法将不可用。

不过在实际使用过程中,发现还用很多的未提及的限制需要小心翼翼地处理。

例如看到示例中的这两句代码:

post_qs = Post.objects.all().order_by().annotate(
        type=Value('p', output_field=CharField(max_length=1)),
        entry_pinned=F('pinned'))
material_qs = Material.objects.all().order_by().annotate(
        type=Value('m', output_field=CharField(max_length=1)),
        entry_pinned=Value(False, BooleanField()))
order_by()Metaordering
django.db.utils.DatabaseError: ORDER BY not allowed in subqueries of compound statements.
annotatePostpinnedpinnedMaterial

有的同学可能想这样做:

post_qs = Post.objects.all().annotate(
    type=Value('p', output_field=CharField(max_length=1)))
post_qs = post_qs.values_list(
        'title','pub_date','pinned','type'
    )

material_qs = Material.objects.all().annotate(
    type=Value('m', output_field=CharField(max_length=1)),
    pinned=Value(False, BooleanField()))
material_qs = material_qs.values_list(
        'title','pub_date','pinned','type'
    )
Postvalues_listvalues_list
SELECT "blog_post"."title", "blog_post"."pub_date", "blog_post"."pinned", 'p' AS "type"
FROM "blog_post"
UNION
SELECT "courses_material"."title", "courses_material"."pub_date", 'm' AS "type", False AS "pinned"
FROM "courses_material"
ORDER BY (4) DESC, (2) DESC
pinnedannotateannotate
material_qs = Material.objects.all().annotate(
    type=Value('m', output_field=CharField(max_length=1)),
    pinned=Value(False, BooleanField()))
typepinnedUNIONtypepinned
annotate
post_qs = Post.objects.all().annotate(
    type=Value('p', output_field=CharField(max_length=1)))
post_qs = post_qs.values_list(
        'title','pub_date','pinned','type'
    )

material_qs = Material.objects.all().annotate(
    pinned=Value(False, BooleanField())).annotate(
    type=Value('m', output_field=CharField(max_length=1)))
material_qs = material_qs.values_list(
        'title','pub_date','pinned','type'
    )
material_qsannotatepinnedtypepinned
ORDER BYnull_firstnulls_last
entry_qs = entry_qs.order_by(F('type').desc(), '-pub_date')
entry_qs = entry_qs.order_by('-pinned', F('pub_date').desc(nulls_last=True))

将得到如下错误:

django.db.utils.DatabaseError: ORDER BY term does not match any column in the result set.

然而对于实际的 SQL 语句,使用 as 设置的别名字段是可以进行排序的:

SELECT "blog_post"."title", "blog_post"."pub_date", "blog_post"."pinned", 'p' AS "type"
FROM "blog_post"
UNION
SELECT "courses_material"."title", "courses_material"."pub_date", False AS "pinned", 'm' AS "type"
FROM "courses_material"
ORDER BY type DESC, pub_date DESC

直接执行这条查询语句可以得到正确的查询结果,不知道为何 django 会报错。

总结

unionUNION
annotate
annotate

-- EOF --