惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

罗磊的独立博客
Apple Machine Learning Research
Apple Machine Learning Research
The Cloudflare Blog
WordPress大学
WordPress大学
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 叶小钗
博客园 - 聂微东
阮一峰的网络日志
阮一峰的网络日志
腾讯CDC
博客园 - 三生石上(FineUI控件)
V
V2EX
有赞技术团队
有赞技术团队
V
Visual Studio Blog
小众软件
小众软件
Jina AI
Jina AI
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - Franky
量子位
T
Tailwind CSS Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
P
Palo Alto Networks Blog
Cisco Talos Blog
Cisco Talos Blog
I
Intezer
Project Zero
Project Zero
A
Arctic Wolf
P
Privacy International News Feed
V
Vulnerabilities – Threatpost
L
Lohrmann on Cybersecurity
S
Securelist
C
Cybersecurity and Infrastructure Security Agency CISA
C
CXSECURITY Database RSS Feed - CXSecurity.com
T
Tor Project blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
S
Security @ Cisco Blogs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Google DeepMind News
Google DeepMind News
N
News and Events Feed by Topic
TaoSecurity Blog
TaoSecurity Blog
L
LINUX DO - 热门话题
G
GRAHAM CLULEY
Help Net Security
Help Net Security
N
News | PayPal Newsroom
W
WeLiveSecurity
G
Google Developers Blog
Microsoft Security Blog
Microsoft Security Blog
Engineering at Meta
Engineering at Meta
MongoDB | Blog
MongoDB | Blog
C
Check Point Blog

追梦人物的博客

0x05:Merkle Tree & Patricia Trie 0x04:ECDSA - 以太坊设计与实现 LeetCode 105 从前序与中序遍历构造二叉树 迭代算法原理 + 完整证明 0x03:Address - 以太坊设计与实现 0x02:Secp256k1 - 以太坊设计与实现 0x00:专栏开篇 - 以太坊设计与实现 0x01:RLP 编码 - 以太坊设计与实现 Bellman-Ford 算法原理及其在 DeFi 套利中的应用 - 追梦人物的博客 迪杰斯特拉(Dijkstra)最短路径算法原理、实现与证明 - 追梦人物的博客 实战:CEX-DEX 稳定币套利监控程序开发 - 追梦人物的博客 CEX-DEX 稳定币套利模型 - 追梦人物的博客 Uniswap 手续费和协议费机制剖析 - 追梦人物的博客 Uniswap 流动性机制及相关数学原理分析 - 追梦人物的博客 uv 替代 pyenv + pipx + poetry 环境管理实践 Rust 项目从创建到发布 - 追梦人物的博客 VS Code 调试 Python - 追梦人物的博客 比特币跨市场套利的数学模型 - 追梦人物的博客 ERC-20 相关知识点总结 - 追梦人物的博客 Django 老项目如何从 SQLite 迁到 PostgreSQL 自动生成接口文档 - HelloDjango - django REST framework 教程 单元测试 - HelloDjango - django REST framework 教程 限制接口访问频率 - HelloDjango - django REST framework 教程 API 版本管理 - HelloDjango - django REST framework 教程 如何在 Windows 下搭建高效的 django 开发环境 拓展Python Markdown - 追梦人物的博客 加缓存为接口提速 - HelloDjango - django REST framework 教程 基于 drf-haystack 实现文章搜索接口 - HelloDjango 评论接口 - HelloDjango - django REST framework 教程 实现分类、标签、归档日期接口 - HelloDjango - django REST framework 教程 在接口返回Markdown解析后的内容 - HelloDjango - django REST framework 教程 文章详情接口 - HelloDjango - django REST framework 教程 分页 - HelloDjango - django REST framework 教程 使用视图集简化代码 - HelloDjango - django REST framework 教程 用类视图实现首页 API - HelloDjango - django REST framework 教程 实现博客首页文章列表 API - HelloDjango - django REST framework 教程 初始化 RESTful API 风格的博客系统 - HelloDjango django-rest-framework 是什么鬼? - HelloDjango - django REST framework 教程 结束 or 开始? - HelloDjango Coverage.py 统计测试覆盖率 - HelloDjango - Django博客教程(第二版) 单元测试:测试评论应用 - HelloDjango - Django博客教程(第二版) 单元测试:测试 blog 应用 - HelloDjango Django Haystack 全文检索与关键词高亮 - HelloDjango Django 博客实现简单的全文搜索 - HelloDjango - Django博客教程(第二版) 开启 Django 博客的 RSS 功能 - HelloDjango 统计各个分类和标签下的文章数 - HelloDjango - Django博客教程(第二版) 稳定易用的 Django 分页库,完善分页功能 - HelloDjango 通过 Django Pagination 实现简单分页 - HelloDjango 在脚本中使用 ORM:Faker 批量生成测试数据 - HelloDjango Django 官方推荐的姿势:类视图 - HelloDjango - Django博客教程(第二版) 开发博客文章阅读量统计功能 - HelloDjango - Django博客教程(第二版) 使用 Docker 让部署 Django 项目更加轻松 - HelloDjango 使用 Certbot 向 Let's Encrypt 免费申请 HTTPS 证书 - HelloDjango 使用 Fabric 自动化部署 - HelloDjango 博客代码开源啦 - 追梦人物的博客 (赠书)推荐一本django书籍:Django企业开发实战 - 追梦人物的博客 Nginx+Gunicorn+Supervisor 部署 Django 博客应用 - HelloDjango 优化博客功能细节,提升使用体验 - HelloDjango - Django博客教程(第二版) 交流的桥梁:评论功能 - HelloDjango - Django博客教程(第二版) 分类、归档和标签页 - HelloDjango - Django博客教程(第二版) 页面侧边栏:使用自定义模板标签 - HelloDjango - Django博客教程(第二版) 自动生成文章摘要 - HelloDjango - Django博客教程(第二版) Markdown 文章自动生成目录,提升阅读体验 - HelloDjango - Django博客教程(第二版) 让博客支持 Markdown 语法和代码高亮 - HelloDjango 开发博客文章详情页 - HelloDjango - Django博客教程(第二版) 创作后台开启,请开始你的表演 - HelloDjango - Django博客教程(第二版) 博客从“裸奔”到“有皮肤” - HelloDjango - Django博客教程(第二版) Django 的接客之道 - HelloDjango - Django博客教程(第二版) Django 迁移、操作数据库 - HelloDjango - Django博客教程(第二版) 创建 Django 博客的数据库模型 - HelloDjango 开始进入 django 开发之旅 - HelloDjango "空空如也"的博客应用 - HelloDjango - Django博客教程(第二版) 一种自顶而下的Python装饰器设计方法 - 追梦人物的博客 Python提取支付宝和微信支付二维码 - 追梦人物的博客 2018 - 我的学生生涯最后一年回顾 - 追梦人物的博客 批量清除todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 筛选练习参考答案 - Vue 2.x Todo 教程练习参考答案 删除todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 编辑todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 添加todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 标为完成练习参考答案 - Vue 2.x Todo 教程练习参考答案 入门仪式_Hello_Vue练习参考答案 - Vue 2.x Todo 教程练习参考答案 组件化todo应用 - Vue 2.x Todo 教程 批量清除todo - Vue 2.x Todo 教程 本地存储 - Vue 2.x Todo 教程 筛选 - Vue 2.x Todo 教程 还剩多少todo未完成 - Vue 2.x Todo 教程 自定义指令实现自动聚焦 - Vue 2.x Todo 教程 全部标为完成 - Vue 2.x Todo 教程 删除todo - Vue 2.x Todo 教程 编辑todo - Vue 2.x Todo 教程 添加todo - Vue 2.x Todo 教程 标为完成 - Vue 2.x Todo 教程 UI - Vue 2.x Todo 教程 显示todo列表 - Vue 2.x Todo 教程 入门仪式:Hello Vue - Vue 2.x Todo 教程 在学习django-rest-framework时收集的学习资料推荐 - 追梦人物的博客 区块链理论与应用研究小组成员招募书 - 追梦人物的博客 Python界网红,豆瓣工程师董伟明加了我的QQ后 - 追梦人物的博客 招募Django学习小组项目组核心成员 - 追梦人物的博客
Django 使用 union 合并不同模型(Model) 的查询集(QuerySet)
2019-10-23 · via 追梦人物的博客

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

一个直观的想法就是将多个查询集合并为一个列表,然后使用 Python 的 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)

但是这种方法需要额外遍历两个 QuerySet,而且排序在 Python 层面进行,会损失一些性能。在对查询到的数据进行操作时的一个重要原则是尽可能在最底层完成操作。例如尽量在数据库层面进行数值计算或者排序等操作,数据库无法完成操作时再上升到 Python 层面。那么使用 Django 的 ORM 有没有办法同时查询出多个模型的数据并对其进行计算或者排序呢?答案是使用查询集的 union 方法。

QuerySet 的 union 方法

union 方法其实对应数据库的 UNION 操作。以我的博客为例(源代码位于 django-blog-project),博客有 2 个 app,其中一个 app 中有一个 Post 模型,用于记录普通类型的博客文章,另一个 app 中有一个 Material 模型,用于记录教程类文章。现在有一个需求,需要查询出全部的 PostMaterial,并以文章发表时间 pub_date 逆序排序(但置顶的普通类型文章必须排在最前面)用于博客首页文章列表展示。2 个模型定义分别定义如下(适当简化,完整定义请参考源码):

from 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']

可以看到 MaterialPost 少了一个 pinned 字段,pinned 字段用于标识文章是否置顶。首页文章展示需要查出除了 body 外的全部字段。ORM 的查询代码如下:

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

除了查询出模型已有字段,还使用 annotate 设置了额外的查询字段,type 用于标识博客文章类型,Material 模型没有 pinned 字段,因此使用 annotate 设置了一个 entry_pinned 字段,其值恒定为 False,同时还对 Post 模型的 pinned 字段名使用 annotate 进行了别名设置,pinned 字段设置别名的具体原因会在后面说。

查看 entry_qsquery 属性,这个 ORM 查询实际执行的 SQL 语句如下:

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

Django 默认使用 UNION 操作,这会去除重复记录,保留重复记录可以给 union 方法传入 all=True,这将使用 UNION ALL 操作。

注意事项

显然,要将两个不同模型的查询集合并为一个查询集,会有一些限制条件,因为涉及数据库的 UNION 操作,至少要保证两个模型查询出来的字段和类型都匹配。下面是 Django 的官方文档给出的 union 方法使用限制。

  • 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() 取消模型的默认排序(模型在 Meta 通过 ordering 选项指定了默认排序),如果不这样做,将得到一个异常:

django.db.utils.DatabaseError: ORDER BY not allowed in subqueries of compound statements.

另外还要注意 annotate 的使用,尽管 Post 模型定义了 pinned 字段,可以直接进行查询,但是这种情况下必须要使用 annotate(对应数据库中的 as 别名)对 pinned 字段取一个别名,因为 Material 模型没有这个字段,但在查询时设置了一个固定值的别名(为了保证查询字段的个数、顺序和类型三者一致)。

有的同学可能想这样做:

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

Post 模型直接通过 values_list 选择需要的字段。看上去两个模型通过 values_list 方法 select 的字段数量、顺序、类型都相同,但实际上 Django 执行的 SQL 却是:

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

注意这里 pinned 的顺序不匹配了,这会导致字段顺序错乱,字段值错位,得不到想要的结果。其原因就是 annotate 的字段顺序不匹配。annotate 方法传入的关键字参数会被收集为字典,而字典是无序的,所以看到这段代码:

material_qs = Material.objects.all().annotate(
    type=Value('m', output_field=CharField(max_length=1)),
    pinned=Value(False, BooleanField()))

typepinned 顺序无法确定,导致 SQL 查询中,UNION 后的那条查询教程类文章的 select 语句中 typepinned 字段顺序无法确定,导致字段不匹配。

解决办法也很简单,按顺序使用 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_qs 查询中先 annotatepinned 字段,然后再是 type 字段,这样在省掉对 pinned 字段设置别名的同时又保持了字段顺序的一致。

还有看到生成的 SQL 语句中的 ORDER BY 子句,Django 使用了查询字段的位置而不是字段名引用待排序的字段。ORM 中无法使用 F 表达式对 annotate 设置的字段进行排序,以及对模型字段,F 表达式排序方法不能设置 null_first 或者 nulls_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 会报错。

总结

查询集的 union 方法可以将不同模型查询结果合并为一个查询集(使用数据库的 UNION 操作),这样可以将两条查询语句合并为一条,减少数据库的查询次数,同时还能在数据库层面对组合的数据进行排序等操作。但使用时要注意:

  1. select 的字段类型必须匹配(字段名可以不同,但排列顺序要一致)
  2. 确保 annotate 方法设置的查询字段顺序一致
  3. 合并后的查询集,很多方法将不可用
  4. 待合并的查询集不能有排序操作
  5. 合并后的查询集不能对 annotate 设置的字段使用 F 表达式
  6. 合并后的查询集排序时不能指定 null 的顺序

-- EOF --