Django 3.2就在眼前,它充满了新的功能。Django的版本通常不是那么令人兴奋(这是一件好事!),但这次在ORM上增加了许多功能,所以我觉得特别有趣
这是我在Django 3.2中最喜欢的功能列表
图片来自Django欢迎页
很多伟大的人都在为这个版本工作,但其中没有一个是我。我包括了每个新功能的门票链接,以表示我对其背后的人的感谢。
内容表
⚙ 用最新版本的Django设置本地环境
要用最新版本的Django设置一个环境,首先要创建一个新的目录和一个虚拟环境。
复制代码
要安装最新版本的Django,你可以使用pip
,或者如果它还没有发布,直接从git上安装。
复制代码
启动一个新的项目和应用程序。
复制代码
将新的应用程序添加到INSTALLED_APPS
,并配置一个PostgreSQL数据库。
复制代码
为了尝试一些新的功能,创建一个Customer
模型。
复制代码
最后,创建DB,生成并应用迁移。
复制代码
很好!现在添加一些随机的客户数据。
复制代码
祝贺你!你现在有了10K个新客户。现在你已经有了10K个新客户,你已经准备好了!
覆盖索引
覆盖索引允许你在索引中存储额外的列。覆盖索引的主要好处是,当查询只使用索引中存在的字段时,数据库可以使用纯索引扫描,这意味着实际的表根本不被访问。这可以使查询更快。
Django 3.2增加了对PostgreSQL覆盖索引的支持。
新的Index.include和UniqueConstraint.include属性允许在PostgreSQL 11+上创建覆盖索引和覆盖唯一约束。
例如,如果你要搜索在某段时间内加入的客户的名字,你可以在joined_at
,并在索引中包括字段name
,创建一个索引。
复制代码
include
参数使其成为一个覆盖索引。
对于只使用字段joined_at
和name
的查询,数据库将能够满足只使用索引的查询。
复制代码
上面的查询找到了2021年2月之前加入的客户的名字。根据执行计划,数据库能够仅仅使用索引来满足查询,甚至不用访问表。这被称为 “仅索引扫描”。
只有索引的扫描一开始可能有点令人困惑。正如PostgreSQL的官方文档中所描述的那样,在PostgreSQL真正能够_只_使用索引之前可能需要一些时间。
但是在PostgreSQL中的任何表扫描都有一个额外的要求:它必须验证每个检索的行对查询的MVCC快照是 “可见的”[…]。可见性信息不存储在索引项中,只存储在堆项中;所以乍一看,似乎每条记录的检索都需要一个堆访问。
另一个检查表页是否可以被当前事务查看的方法是检查表的可见性地图,它比表本身小得多,访问速度也快得多。PostgreSQL可能需要一些时间来更新可见性地图,所以在那之前,你可能会看到一个像这样的执行计划。
复制代码
为了检查你的索引是否真的可以用于只扫描索引,你可以通过在表上手动发出真空分析来加速这一过程。
复制代码
执行VACUUM
,也会回收一些未使用的空间,使其可以重新使用。
2020-03-04更新。我最初建议使用VACUUM FULL
,而不是普通的VACUUM
。在twitter上的一个评论者提到,使用VACUUM
就可以达到这个目的,而且干扰性和破坏性会小很多,所以用这个代替吧!
同样重要的是要记住,包容性的索引并不是免费的。索引中的额外字段会使索引变大。
为TruncDate提供时区
我写了很多关于SQL中的错误,而时区通常是在列表的顶部。在处理时间戳时,最危险的错误之一是在没有明确指定时区的情况下进行截断,这可能导致不正确和不一致的结果。
在Django 3.2中,避免这种错误变得更加容易。
TruncDate和TruncTime数据库函数的新参数tzinfo允许在特定的时区截断数据时间。
在以前的Django版本中,时区是根据当前时区在内部设置的。
复制代码
从Django 3.2开始,你可以明确地提供一个时区给 TruncDate
函数系列。
复制代码
这是向正确方向迈出的一步。
构建JSON对象
在PostgreSQL中构建JSON对象是非常方便的,尤其是当你在处理非结构化数据时。
从Django 3.2开始,ORM中加入了PostgreSQL中接受任意键值对的函数json_build_object
。
增加了JSONObject数据库函数。
一个有趣的用例是直接在数据库中序列化对象,绕过了创建ORM对象的需要。
复制代码
我们已经展示了序列化性能的重要性,所以这是一个值得考虑的问题。
响亮的信号接收器
不久前,我在推文中提到了一个神秘的bug,由于它发生在一个信号接收器内部,所以很长时间都没有被注意到。
当你使用send_robust
来广播信号时,如果信号失败了,Django会保留这个错误并转到下一个接收器。在所有的接收器处理完信号后,Django会返回一个包含接收器返回值和异常的列表。为了检查是否有接收者失败,你需要查看该列表并检查Exception
的实例。信号通常被用来解耦模块,而用这种方式把异常从接收者那里传递出去就违背了这个目的。
为了确保我不会再错过信号接收器中的异常,我创建了一个 “响亮的信号接收器 “来记录异常。
复制代码
从Django 3.2开始,这就不再需要了。
Signal.send_robust()现在会记录异常。
很好!
查询集的别名
alias
函数是Django 3.2中一个全新的功能。
新的QuerySet.alias()方法允许为表达式创建可重复使用的别名,这些表达式不需要被选择,但可用于过滤、排序,或作为复杂表达式的一部分。
我经常使用SubQuery
和OuterRef
来编写复杂的查询,当与annotate
结合时,有一个小问题。
复制代码
上面的查询是一个复杂的方法来寻找第一个加入的客户。该查询集使用SubQuery
,通过joined_at
,为每个客户找到前一个客户,然后寻找之前没有其他客户加入的客户。这是非常低效的,但我用它来说明我的观点。
为了理解这个问题,请检查这个queryset产生的查询。
复制代码
注释的子查询同时出现在SELECT
和WHERE
子句中。这影响了执行计划。
复制代码
子查询被执行了两次!
为了解决这个问题,在3.2之前的Django版本中,你可以提供一个values_list
,将被注释的子查询从SELECT
子句中排除。
复制代码
题外话:你可能认为,在这种情况下,你可以使用.defer('id_of_previous_customer')
,而不是使用values_list
,省略被注释的字段。这是不可能的。Django会向你抛出一个KeyError: 'id_of_previous_customer'
。
从Django 3.2开始,你可以用alias
替换annotate
,这个字段将不会被添加到选择子句中。
复制代码
现在生成的SQL只使用一次子查询。
复制代码
执行计划更简单了。
复制代码
少了一个让你自投罗网的方法!
新的管理装饰器
在Django 3.2之前,要在Django管理中定制一个计算字段,首先要添加一个函数,然后给它分配一些属性。
复制代码
这就是那种奇怪的API,大多只有在Python这样的动态语言中才能实现。
如果你使用的是Mypy(你应该这样做),这段代码会触发一个恼人的警告,而让它安静下来的唯一方法是添加一个type: ignore
。
复制代码
如果你像我一样经常使用Django Admin和Mypy,这可能是相当恼人的。
新的display
装饰器解决了这个问题。
新的display()装饰器可以轻松地将选项添加到可用于list_display或readonly_fields的自定义显示函数中。 同样地,新的action()装饰器可以轻松地将选项添加到可用于action的函数中。
调整代码以使用新的display
装饰器。
复制代码
没有类型错误!
另一个有用的装饰器是 action
它使用类似的方法来定制自定义的管理动作。
值表达式检测类型
这是个小功能,解决了ORM中的一个小麻烦。
现在,Value()表达式会根据其提供的值的类型自动将其output_field解析为适当的Field子类,适用于bool、bytes、float、int、str、datetime.date、datetime.datetime、datetime.time、datetime.timedelta、decimal.Decimal和uuid.UUID实例。因此,在使用Value()时,解析数据库函数和组合表达式的output_field现在可能会因混合类型而崩溃。在这种情况下,你需要明确设置output_field。
在以前的Django版本中,如果你想在查询中使用一些常量值,你必须明确设置一个output_field
,否则会失败。
复制代码
在Django 3.2中,ORM自己就能解决这个问题。
复制代码
非常酷!
更多值得一提的功能
Django 3.2中还有很多功能,文档解释得比我好。仅举几例。
-
管理中的可导航链接(Ticket #31181)。如果在管理员中注册了目标模型,只读的相关字段现在会呈现为可导航的链接。我还在用装饰器来给Django Admin添加链接,估计现在我对它的使用会减少。
-
atomic()
(Ticket #32220)的持久性参数。当你在数据库事务中执行代码时,当事务完成时没有任何错误,你希望它被提交到数据库中。然而,如果调用者在他自己的数据库事务中执行你的代码,如果父事务被回滚,你的代码也会被回滚。为了防止这种情况发生,你现在可以把你的事务标记为durable
。当有人试图在另一个事务内打开一个持久的事务时,会产生一个RuntimeError
错误。 -
缓存的模板会在Django的开发服务器上重新加载(Ticket #25791)。如果你使用Django的
runserver
命令进行本地开发,你可能已经习惯了当一个python文件发生变化时重新加载。然而,如果你使用的是Django的django.template.loaders.cached.Loader
loader,当一个HTML文件发生变化时,开发服务器不会重新加载它,你必须重新启动开发服务器才能看到变化。这是很烦人的,到目前为止,我不得不在dev中禁用缓存加载器。从Django 3.2开始,这就没有必要了,因为缓存的模板在开发中可以正确地重新加载。 -
支持基于函数的索引(Ticket #26167)。当你经常查询一个表达式,并且你想对其进行索引时,FBI就很有用。一个典型的例子是为小写字母文本建立索引。
愿望清单
Django ORM已经很全面了,功能也很丰富,但在我的愿望清单上仍有一些东西需要未来的版本。
-
自定义连接。Django目前只能在通过
ForeignKey
连接的表之间进行连接。在有些情况下,你想连接那些不一定用外键连接的表,或者使用更复杂的条件。一个常见的例子是缓慢变化的维度,其中的连接条件需要一个BETWEEN
操作符。 -
更新返回。当更新许多行时,有时立即获取它们是很有用的。这是SQL中一个众所周知的(也是一个非常有用的)功能。Django目前不支持这个功能,但我听说它很快就会支持。
-
数据库视图。有许多黑客可以让数据库视图与ORM一起工作。这些方法通常涉及到直接在数据库中或在手动迁移中创建一个视图,然后在模型上设置
managed=False
在模型上。这些小技巧可以完成工作,但不是以一种非常优雅的方式。我希望有一种方法可以定义数据库视图,以便迁移可以检测到变化。甚至可以选择使用Django的queryset来创建视图。 -
数据库分区。数据库分区在数据建模中是非常有用的。如果使用得当,它们可以使查询更快,维护更容易。一些数据库引擎,如Oracle,已经为数据库分区提供了非常成熟的实现,其他引擎如PostgreSQL也在逐步实现。目前,Django中还没有对数据库分区的原生支持,我所看到的大多数实现都是采用手动管理表的方式。因此,我经常避免分区,这是很不幸的。
-
要求默认情况下的认证。Django目前允许对任何视图进行访问,除非明确标注,通常使用
require_login
装饰器。这使得Django更容易上手,但如果你不小心的话,就有可能导致安全问题。我知道有一些解决方案,通常使用自定义的中间件和装饰器。我真的希望Django能有一个选项来翻转这个条件,使访问被默认_限制_,除非另有标记。 -
打字。如果你关注这个博客,你就知道我是Python中类型提示的忠实粉丝。目前,Django并没有提供类型提示或官方存根。诸如Starlette和FastAPI这样闪亮的新框架标榜自己是100%类型注释的,但Django仍然落后。有一个名为django-stubs的项目在这方面取得了一些进展。
-
数据库连接池Django目前支持两种管理数据库连接的模式–为每个请求创建一个新的连接,或者为每个线程创建一个新的连接(持久化连接)。在常见的部署中,创建数据库连接是一个相对沉重的操作。它需要设置一个TCP连接,通常是一个TLS连接,并初始化连接,这增加了大量的延迟。特别是在PostgreSQL中,它还会消耗很多数据库服务器的资源,所以为每个请求创建一个新的连接是一个非常糟糕的主意。
持久连接要好得多。它们在Django通常的部署方式下运行良好,即少量的工作进程和/或线程。但是这样的部署在现实世界中往往会崩溃。每当你的数据库或你的一个上游由于某种原因开始需要更长的时间来处理请求时,工作者就会被束缚住,请求就会回流,整个系统就会窒息。即使有严格的超时,这仍然会发生。
为了改善这种灾难性的失败模式,一个常见的解决方案是使用同步工作者,如gevent greenlets,或在未来使用asycnio任务。但是现在,每个请求都有自己的轻量级线程,因此有自己的连接,这使得Django的持久化连接功能变得毫无用处。
如果Django包含一个高质量的连接池,它可以维护一定数量的连接,并根据需要将其分配给请求,那就太好了。像PgBouncer这样的外部解决方案是存在的,但它们会增加操作的开销。一个内置的解决方案通常就足够了。