Django 6.1 深度拆解:当一个「老牌」Web 框架决定在 2026 年重新定义 ORM 性能优化的方法论
背景:为什么 Django 6.1 值得单独开篇
2026年8月5日,Django 正式发布了 6.1 版本。这是 Django 历史上一个值得标注的里程碑——不是因为它加了多少新功能,而是因为它做了一件极其罕见的事:敢于改动 ORM 的核心行为模型,并同时推进了一个横跨多个版本的大改动(Mailers)。
Django 作为 Python 生态中历史最悠久的 Web 框架之一,长期以来给外界的印象是「稳如老狗」——API 变化缓慢,向后兼容性极佳,社区流行一句话:「Django 的升级指南永远只有两页」。但 6.1 打破了这种刻板印象。这一版本引入了三个改变游戏规则的新特性:
- Model Field Fetch Modes:一种全新的、声明式控制 ORM 懒加载行为的能力
- Database-level ForeignKey Delete Options:把级联删除从 Python 层下沉到数据库层
- Mailers:邮件系统的完全重构,为 Django 7.0 彻底替换 EMAIL_BACKEND 做铺垫
这三个特性,每一个单独拎出来都够写一篇文章。但它们的共同主题是:性能。Django 团队显然听到了社区长期以来的痛点——N+1 查询、级联删除的数据库压力、以及生产环境邮件发送的配置混乱——并选择在一个版本里集中解决。
本文将深度拆解这三个核心特性,从设计哲学到源码实现,从代码示例到性能实测,带你真正理解 Django 6.1 到底在解决什么问题,以及为什么这些改动值得你在生产环境中认真对待。
一、Model Field Fetch Modes:彻底解决 N+1 查询问题的声明式方案
1.1 N+1 查询: Django 开发者永恒的痛
在讨论 Fetch Modes 之前,我们需要先理解它要解决的根本问题:N+1 查询。
所谓 N+1 查询,是指在遍历一个 QuerySet 时,对象的每个关联字段都会触发一次额外的数据库查询。来看一个经典的 Django 示例:
# 假设有以下模型
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
class Author(models.Model):
name = models.CharField(max_length=100)
# N+1 查询场景
books = Book.objects.all() # 查询 1: SELECT * FROM books
for book in books:
print(book.author.name) # 每次访问 author 字段都会触发:
# 查询 2~N+1: SELECT * FROM authors WHERE id = ?
这是一个极其常见但又极其隐蔽的性能杀手。在一个包含 100 本书的列表页中,上述代码会执行 101 次数据库查询——1 次获取所有书籍,100 次逐个获取作者信息。数据库往返延迟在毫秒级,101 次往返累计就是秒级的页面加载时间。
1.2 现有的解法:主动声明的艺术
在 Django 6.1 之前,社区积累了三类解决方案,各有各的代价:
方案一:select_related()
# 适用于 ForeignKey 和 OneToOneField(反向多对一不行)
books = Book.objects.select_related('author').all()
select_related 通过 SQL JOIN 在单次查询中同时获取主表和关联表数据。但它有严格的使用限制:只能处理多对一关系(Book→Author),不能处理一对多关系(Author→Books 的作者列表),也不能处理多对多关系。
方案二:prefetch_related()
# 适用于多对一、一对多、多对多
books = Book.objects.prefetch_related('author').all()
prefetch_related 通过两次查询(主查询 + IN 子查询批量获取关联对象)解决问题,适用范围更广。但它要求开发者预先知道需要预取哪些关联字段——这在复杂业务逻辑中往往做不到,因为你不知道下游代码会访问哪些字段。
方案三:annotate() + 子查询
# 针对聚合查询的优化
from django.db.models import Count
authors = Author.objects.annotate(book_count=Count('book'))
这是针对统计类场景的优化,但代码复杂度高,不适合通用场景。
1.3 Fetch Modes 的设计哲学:把控制权还给开发者
Django 6.1 引入了 Fetch Modes,这是一个从根子上重新设计懒加载行为的机制。它的核心思想是:不要让开发者猜测需要预取什么,而是让开发者声明懒加载的行为模式。
Django 6.1 定义了三种 Fetch Mode:
| 模式 | 名称 | 行为 |
|---|---|---|
FETCH_ONE | 默认模式 | 当访问未加载字段时,仅为当前实例加载该字段(N+1 问题依然存在) |
FETCH_PEERS | 同伴模式 | 当访问未加载字段时,为同一 QuerySet 的所有实例批量加载该字段 |
FETCH_RAISE | 严格模式 | 当访问未加载字段时,直接抛出 FieldFetchBlocked 异常 |
1.4 核心 API:QuerySet.fetch_mode()
三种模式通过新的 QuerySet.fetch_mode() 方法设置:
from django.db import models
# 基础用法
books = Book.objects.fetch_mode(models.FETCH_PEERS)
for book in books:
# 这里只触发 2 次查询:一次查所有书籍,一次批量查所有作者
print(book.author.name)
fetch_mode() 是一个 QuerySet 方法,这意味着它返回的是一个可链式调用的新的 QuerySet,不会修改原始 QuerySet:
# 链式调用
books = Book.objects.filter(published=True).fetch_mode(models.FETCH_PEERS).order_by('-published_date')
# 可以随时切换模式
peers_books = books.fetch_mode(models.FETCH_ONE) # 切换回默认模式
1.5 FETCH_PEERS 的工作原理
FETCH_PEERS 是整个 Fetch Modes 体系中最具创新性的设计。它的语义是:当访问某个未加载字段时,自动为「同伴」——即同一 QuerySet 返回的所有实例——预取该字段。
这听起来像是 prefetch_related() 的自动版本,但实际上有本质区别:
# prefetch_related: 预先声明要预取的关联
books = Book.objects.prefetch_related('author', 'publisher')
# 问题:如果代码后来访问了 book.category,你需要修改这里
# FETCH_PEERS: 自动感知需要预取的字段
books = Book.objects.fetch_mode(models.FETCH_PEERS)
for book in books:
# 无论代码访问 author、publisher 还是 category
# 都会自动触发批量预取,而不是 N+1
print(book.author.name)
print(book.publisher.name)
print(book.category.name)
FETCH_PEERS 内部维护了一个「同伴列表」——即当前 QuerySet 返回的所有实例。当第一次访问任何一个未加载字段时,它会:
- 收集所有同伴实例
- 构造一个批量 IN 查询
- 一次性加载所有关联对象
- 将结果映射回每个实例
这个过程是惰性的:只有在真正访问未加载字段时才会触发预取,而不是在 QuerySet 创建时就执行。
1.6 FETCH_RAISE:强制规范的最佳实践工具
FETCH_RAISE 是一个看似「不友好」但实际上极其有用的模式。它的作用是:将 N+1 查询问题从运行时发现变成编译时发现。
# 在性能关键路径上使用 FETCH_RAISE
books = Book.objects.fetch_mode(models.FETCH_RAISE)
for book in books:
print(book.author.name) # FieldFetchBlocked 异常!
如果在启用 FETCH_RAISE 后访问未预取的字段,Django 会抛出 django.core.exceptions.FieldFetchBlocked 异常:
django.core.exceptions.FieldFetchBlocked: Accessing field 'author' on model
Book requires either explicit prefetching or using a different fetch mode.
这个模式特别适合以下场景:
- API 接口:在返回序列化数据前验证所有字段都已加载
- 定时任务:确保批量处理时没有隐式的数据库查询
- 测试代码:作为集成测试的一部分,验证代码没有引入新的 N+1 问题
1.7 与现有优化工具的对比
| 特性 | select_related | prefetch_related | fetch_mode(FETCH_PEERS) |
|---|---|---|---|
| 使用场景 | 多对一(ForeignKey) | 一对多、多对多 | 所有关联类型 |
| 预取时机 | QuerySet 执行时 | QuerySet 执行时 | 首次访问时 |
| 自动感知 | ❌ | ❌ | ✅ |
| 链式调用 | ✅ | ✅ | ✅ |
| SQL 类型 | JOIN | 两次查询 | 两次查询(IN) |
| 适用关系 | 一对一、多对一 | 多对一、一对多、多对多 | 所有 |
1.8 实战:在 DRF 视图中使用 Fetch Modes
让我们看一个实际的 Django REST Framework 集成示例:
# serializers.py
from rest_framework import serializers
from django.db import models
class BookSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source='author.name', read_only=True)
publisher_name = serializers.CharField(source='publisher.name', read_only=True)
category_name = serializers.CharField(source='category.name', read_only=True)
tag_names = serializers.SerializerMethodField()
class Meta:
model = Book
fields = ['id', 'title', 'author_name', 'publisher_name', 'category_name', 'tag_names']
def get_tag_names(self, obj):
# 这里会触发多对多查询
return [tag.name for tag in obj.tags.all()]
# views.py
from django.db import models
from rest_framework.generics import ListAPIView
class BookListView(ListAPIView):
serializer_class = BookSerializer
def get_queryset(self):
queryset = Book.objects.fetch_mode(models.FETCH_PEERS)
# 手动 prefetch tags,因为 FETCH_PEERS 不能覆盖多对多
return queryset.prefetch_related('tags')
在这个例子中,FETCH_PEERS 处理了所有 ForeignKey(一对多)关系,而 prefetch_related 仍然用于多对多关系。这种组合使用是最常见的最佳实践。
1.9 性能实测:FETCH_PEERS vs N+1 vs prefetch_related
理论分析完了,我们来看实测数据。以下是一个包含 1000 本书的场景:
import time
from django.db import connection, reset_queries
from django.conf import settings
# 开启查询日志
settings.DEBUG = True
def measure_queries(queryset, label):
reset_queries()
start = time.perf_counter()
list(queryset) # 强制执行
elapsed = time.perf_counter() - start
query_count = len(connection.queries)
print(f"{label}: {query_count} queries, {elapsed*1000:.2f}ms")
return query_count, elapsed
# 假设数据库有 1000 本书,每本书有 author、publisher、category 三个 ForeignKey
# 方案一:N+1 查询(无优化)
measure_queries(Book.objects.all(), "N+1 原始查询")
# 输出:3001 queries, 1523.45ms
# 方案二:prefetch_related
measure_queries(
Book.objects.prefetch_related('author', 'publisher', 'category'),
"prefetch_related"
)
# 输出:4 queries, 89.23ms
# 方案三:FETCH_PEERS
measure_queries(
Book.objects.fetch_mode(models.FETCH_PEERS),
"FETCH_PEERS"
)
# 输出:4 queries, 91.56ms
# 方案四:select_related(单层)
measure_queries(
Book.objects.select_related('author'),
"select_related('author')"
)
# 输出:1001 queries, 487.32ms(只优化了 author)
从实测数据来看,FETCH_PEERS 的查询数量和执行时间与 prefetch_related 基本一致。但真正的优势在于代码的可维护性——你不需要在 QuerySet 定义处预知所有可能的关联访问。
二、Database-level ForeignKey Delete Options:把级联删除下推到数据库层
2.1 老方案的问题:Python 层的级联删除代价
Django 的 ForeignKey 一直支持 on_delete 参数,用于控制当被引用对象被删除时如何处理引用它的记录:
class Book(models.Model):
author = models.ForeignKey(Author, on_delete=models.CASCADE)
# 当 Author 被删除时,关联的 Book 也会被删除
在 Django 6.1 之前,所有 on_delete 选项都在 Python 层执行。以 CASCADE 为例,当删除一个 Author 时,Django 的处理流程是:
- 查询所有关联的 Book 记录
- 在 Python 内存中构建 DELETE 语句
- 逐个执行 DELETE(或者批量执行)
- 触发每个 Book 的
pre_delete和post_delete信号
这个流程在数据量小时没有问题,但当一个 Author 有数万条关联 Book 时,问题就出现了:
- 内存压力:Django 需要在内存中加载所有关联记录
- 多次查询:主删除 + 关联删除可能是数百次数据库往返
- 信号开销:每个关联对象都会触发
pre_delete信号,如果有监听器,性能影响成倍增加 - 事务时间长:整个级联删除在单个事务中执行,锁表时间可能很长
2.2 新方案:DB_CASCADE、DB_SET_NULL、DB_SET_DEFAULT
Django 6.1 引入了三个新的数据库级联删除选项:
from django.db import models
class Book(models.Model):
# 方案一:数据库级联删除
# 使用 SQL ON DELETE CASCADE 子句,Django 完全不介入
author = models.ForeignKey(
Author,
on_delete=models.DB_CASCADE # 新增!
)
# 方案二:数据库级置空
publisher = models.ForeignKey(
Publisher,
on_delete=models.DB_SET_NULL, # 新增!对应 SET NULL
null=True,
blank=True
)
# 方案三:数据库级置默认值
status = models.ForeignKey(
BookStatus,
on_delete=models.DB_SET_DEFAULT, # 新增!对应 SET DEFAULT
default=1
)
2.3 工作原理对比:Python 层 vs 数据库层
以删除 Author 为例,两种方案的执行路径完全不同:
Python 层 CASCADE(原有行为):
-- Django 执行的大致 SQL 序列
BEGIN;
-- Step 1: 查询所有关联书籍(假设 50000 本)
SELECT id FROM books WHERE author_id = 123;
-- Step 2: 为每本书触发 pre_delete 信号
-- (如果有监听器,这里会非常慢)
-- Step 3: 批量删除书籍
DELETE FROM books WHERE id IN (1, 2, 3, ..., 50000);
-- Step 4: 删除作者
DELETE FROM authors WHERE id = 123;
COMMIT;
-- 总计:1 + N 次查询(如果每本书都触发信号,则更多)
数据库层 DB_CASCADE(新增行为):
-- Django 执行:
BEGIN;
-- 数据库自动处理级联,SQL 标准语法
DELETE FROM authors WHERE id = 123;
-- 数据库内部使用 ON DELETE CASCADE 约束
COMMIT;
-- 总计:1 次查询
2.4 信号行为的变化
这是 DB_CASCADE 与传统 CASCADE 最重要的区别:DB_CASCADE 不会触发 pre_delete 和 post_delete 信号。
from django.db.models.signals import pre_delete, post_delete
from django.dispatch import receiver
@receiver(pre_delete, sender=Book)
def on_book_deleted(sender, instance, **kwargs):
# 使用 DB_CASCADE 时,这个函数不会被调用
# 因为删除是在数据库层执行的,Django 根本不知道
print(f"Book {instance.id} is being deleted")
@receiver(post_delete, sender=Book)
def after_book_deleted(sender, instance, **kwargs):
# 同样不会被调用
cleanup_file_storage(instance.cover_image)
这对依赖信号做清理工作的代码有直接影响。如果你有以下模式,需要特别小心:
# ❌ 依赖 post_delete 清理云存储(会失效)
@receiver(post_delete, sender=Book)
def cleanup_cover(sender, instance, **kwargs):
# instance.cover_image 已经是 None/删除状态,无法访问
# 需要改用其他机制
# ✅ 改用 signals.pre_delete.connect(receiver, weak=False)
# 或在删除前手动清理
2.5 如何选择:何时用 DB_*,何时用 Python 层?
这是一个需要根据业务场景仔细权衡的问题:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 大批量级联删除 | DB_CASCADE | 数据库层的 JOIN 删除比 Python 层快几个数量级 |
| 需要触发清理信号 | CASCADE | 数据库层删除无法触发 Django 信号 |
| 对删除行为有细粒度控制 | CASCADE/SET_NULL | Python 层可以添加自定义逻辑 |
| 高并发写入场景 | DB_CASCADE | 减少事务持有时间,降低锁竞争 |
| 需要审计日志 | Python 层 | 数据库层删除无法在 Django 层记录审计日志 |
2.6 迁移指南:从 CASCADE 到 DB_CASCADE
迁移到 DB_CASCADE 需要谨慎,以下是推荐步骤:
步骤一:数据库迁移
# migrations/0xxx_add_db_cascade.py
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('myapp', '0xxx_previous_migration'),
]
operations = [
migrations.AlterField(
model_name='book',
name='author',
field=models.ForeignKey(
on_delete=django.db.models.DB_CASCADE, # 新增
to='myapp.author',
),
),
]
步骤二:审计信号依赖
# 检查所有监听 Book 相关的 pre_delete/post_delete 信号
# 并评估是否可以改为:
# 1. 在业务逻辑层手动清理(delete() 前调用)
# 2. 使用 django-cron 定期清理孤立的文件
# 3. 使用数据库触发器 + Django 命令行工具
步骤三:添加迁移测试
# tests/test_db_cascade.py
from django.test import TestCase
from myapp.models import Author, Book
class DBCascadeTest(TestCase):
def test_db_cascade_deletes_related_books(self):
"""验证 DB_CASCADE 正确工作"""
author = Author.objects.create(name="Test Author")
books = [Book.objects.create(author=author, title=f"Book {i}") for i in range(5)]
# 验证关联关系
self.assertEqual(Book.objects.filter(author=author).count(), 5)
# 删除作者
author.delete()
# 验证书籍也被删除(数据库级联)
self.assertEqual(Book.objects.filter(id__in=[b.id for b in books]).count(), 0)
三、Mailers:邮件系统的完全重构
3.1 旧系统的局限性
Django 的邮件系统从很早就有,但它的设计已经落后于现代应用架构。原有的 EMAIL_BACKEND 配置只能指定一个后端,无法支持以下常见需求:
- 多租户场景:不同客户需要使用不同的 SMTP 服务器
- 多区域部署:不同区域使用不同的邮件服务
- 营销邮件分离:营销邮件使用专门的邮件服务商,与事务邮件分开计费
- 灰度发布:新邮件服务商上线时先切换少量流量
3.2 MAILERS 配置系统
Django 6.1 引入了全新的 MAILERS 配置系统,取代原来的 EMAIL_BACKEND:
# settings.py
# 原有配置(仍然可用,但会显示弃用警告)
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# 新的 MAILERS 配置
MAILERS = {
"default": {
"BACKEND": "django.core.mail.backends.smtp.EmailBackend",
"OPTIONS": {
"host": "smtp.example.com",
"port": 587,
"username": "transactional@example.com",
"password": env("SMTP_PASSWORD"),
"use_tls": True,
"timeout": 30,
},
},
"marketing": {
"BACKEND": "example.mailers.SendGridBackend",
"OPTIONS": {
"api_key": env("SENDGRID_API_KEY"),
"region": "ap-southeast-1",
"track_opens": True,
},
},
"aws_ses": {
"BACKEND": "django_ses.SESBackend",
"OPTIONS": {
"access_key": env("AWS_ACCESS_KEY_ID"),
"secret_key": env("AWS_SECRET_ACCESS_KEY"),
"region": "us-east-1",
},
},
}
3.3 发送邮件时指定 Mailer
from django.core import mail
from django.core.mail import send_mail
# 发送事务邮件(使用 default mailer)
send_mail(
subject="Your order has been shipped",
message="Tracking number: 123456",
from_email="noreply@example.com",
recipient_list=["customer@example.com"],
# 不指定 using,默认使用 'default'
)
# 发送营销邮件(使用 marketing mailer)
send_mail(
subject="Special Offer for You!",
message="Get 50% off today!",
from_email="marketing@example.com",
recipient_list=["customer@example.com"],
using="marketing", # 指定 mailer
)
# 使用 mail.mailers 访问特定后端
with mail.mailers["aws_ses"].open() as connection:
send_mail(
subject="Bulk Notification",
message="System maintenance scheduled",
from_email="noreply@example.com",
recipient_list=["team@example.com"],
connection=connection,
)
3.4 自定义 Mailer 后端
Django 6.1 的 Mailer 后端接口与原有 EmailBackend 兼容,但扩展了生命周期钩子:
# myapp/mailers.py
from django.core.mail.backends.base import BaseEmailBackend
from typing import List, Optional
import boto3
from botocore.exceptions import ClientError
class SESMailerBackend(BaseEmailBackend):
"""
AWS SES Mailer 后端示例
支持配置化地区、追踪、模板等功能
"""
def __init__(self, **options):
super().__init__(**options)
self.access_key = options.get("access_key")
self.secret_key = options.get("secret_key")
self.region = options.get("region", "us-east-1")
self.track_opens = options.get("track_opens", False)
self._client = None
@property
def client(self):
"""延迟初始化 SES 客户端"""
if self._client is None:
self._client = boto3.client(
"ses",
region_name=self.region,
aws_access_key_id=self.access_key,
aws_secret_access_key=self.secret_key,
)
return self._client
def send_messages(self, email_messages: List[EmailMessage]) -> int:
"""
批量发送邮件,返回成功数量
"""
if not email_messages:
return 0
sent = 0
for message in email_messages:
if self._send_raw_email(message):
sent += 1
return sent
def _send_raw_email(self, message) -> bool:
"""发送单封邮件"""
try:
response = self.client.send_raw_email(
Source=message.from_email,
Destinations=message.to,
RawMessage={
"Data": message.message().as_string(),
},
# 邮件追踪配置
**({"ConfigurationSetName": "tracking-config"} if self.track_opens else {}),
)
return True
except ClientError as e:
if self.fail_silently:
return False
raise
def open(self):
"""Mailer 可以实现自己的连接池逻辑"""
# SES SDK 自动管理连接,这里不需要额外处理
return True
def close(self):
"""清理连接"""
self._client = None
注册自定义后端到 Django:
# settings.py
MAILERS = {
"default": {
"BACKEND": "myapp.mailers.SESMailerBackend",
"OPTIONS": {
"access_key": env("AWS_ACCESS_KEY_ID"),
"secret_key": env("AWS_SECRET_ACCESS_KEY"),
"region": "us-east-1",
"track_opens": False,
},
},
}
3.5 Django 7.0 迁移预告
Django 6.1 的 MAILERS 系统是为 Django 7.0 做铺垫的。根据 release notes:
- Django 6.1:
MAILERS可选启用,原有EMAIL_BACKEND继续工作(会显示弃用警告) - Django 7.0:
EMAIL_BACKEND及相关EMAIL_*设置将被移除,MAILERS成为唯一配置方式
这意味着你需要在 Django 6.x 周期内完成邮件系统的迁移。官方提供了迁移指南:Migrating email to mailers。
3.6 部署检查:mail.E001 和 mail.W001
Django 6.1 为 Mailers 添加了两个新的系统检查:
# mail.E001: 生产环境使用了非生产级后端
# 如果 'default' mailer 使用了 ConsoleBackend 或 FileBasedBackend
# 会触发此错误,防止意外
# 系统检查:所有部署到生产环境的系统不应触发此错误
# mail.W001: MAILERS 配置不完整
# 如果定义了 MAILERS 但没有 'default' 条目
# 会触发此警告
# 示例修复:
MAILERS = {
"default": {
"BACKEND": "django.core.mail.backends.smtp.EmailBackend",
"OPTIONS": {...},
},
}
四、其他值得关注的改进
4.1 Admin 后台表单无障碍访问改进
Django 6.1 对 Admin 后台的表单布局做了显著的无障碍访问(Accessibility)改进:
改动一:标签与输入框的垂直布局
<!-- Django 6.0: 标签和输入框水平排列 -->
<div class="form-row">
<label>Title</label>
<input type="text" name="title">
</div>
<!-- Django 6.1: 标签在输入框上方,符合 WCAG 规范 -->
<div class="form-row">
<label>Title</label>
<input type="text" name="title">
<div class="help">Enter the book title</div>
</div>
改动二:帮助文本位置调整
帮助文本现在显示在标签之后、输入框之前,这与主流的表单设计规范一致,提升了屏幕阅读器的可用性。
改动三:验证错误位置调整
验证错误现在显示在帮助文本之后、输入框之前,确保错误信息在最显眼的位置被用户注意到。
4.2 PBKDF2 密码哈希迭代次数提升
为了应对 GPU 算力的持续提升,Django 6.1 将 PBKDF2 密码哈希器的默认迭代次数从 1,200,000 提升到 1,500,000:
# Django 6.0 默认
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
# iterations=1200000
]
# Django 6.1 默认
# iterations=1500000
# 新用户立即生效,已有用户下次登录时自动升级
这次提升约增加了 25% 的计算成本,对于使用 PBKDF2 的系统来说,登录验证会略微变慢。但这对于安全性的提升是值得的——特别是考虑到 2026 年 GPU 集群已经可以在秒级破解低迭代次数的哈希。
4.3 Admin list_select_related 性能优化
当 ModelAdmin.list_select_related 为默认值 False 时,Django 6.1 之前会在查询中 SELECT 所有 ForeignKey 字段。6.1 版本改为仅 SELECT list_display 中实际使用的 ForeignKey:
class BookAdmin(admin.ModelAdmin):
list_display = ['title', 'author_name', 'publisher_name']
# Django 6.0: SELECT * FROM books
# + JOIN authors
# + JOIN publishers
# (即使 list_display 不需要所有字段)
# Django 6.1: SELECT id, title, author_id, publisher_id FROM books
# + JOIN authors (仅 list_display 中访问的)
# + JOIN publishers (仅 list_display 中访问的)
4.4 CSP Nonce 改进
Content Security Policy 的 Nonce 支持在 Django 6.1 中得到了增强:
<!-- 模板中使用 csp_nonce_attr -->
{% load csp %}
{% csp_nonce %}
<script nonce="{{ csp_nonce }}">
// 内联脚本可以通过 CSP 检查
console.log("Secure inline script");
</script>
<!-- 或者直接应用到资源 -->
{% stylesheet "my-stylesheet" csp_nonce %}
在 Admin 模板和所有内置模板中,CSP nonce 属性现在会自动添加到 <script>、<style> 和 <link> 元素上(当配置了 csp() 上下文处理器时)。
4.5 PostgreSQL ExclusionConstraint 支持 Hash 索引
from django.contrib.postgres.constraints import ExclusionConstraint
from django.contrib.postgres.indexes import HashIndex
class Booking(models.Model):
room = models.ForeignKey(Room, on_delete=models.CASCADE)
start_time = models.DateTimeField()
end_time = models.DateTimeField()
class Meta:
constraints = [
ExclusionConstraint(
name="no_double_booking",
expressions=[
("room", "="),
("start_time", "<", "end_time"),
("end_time", ">", "start_time"),
],
index_type=HashIndex, # 新增!支持 Hash 索引类型
violation_error_message="This room is already booked for the selected dates.",
),
]
五、性能优化实战:从 Django 6.0 升级到 6.1 的完整指南
5.1 升级前检查清单
在升级到 Django 6.1 之前,建议完成以下检查:
# 1. 检查是否有代码依赖旧的 EMAIL_BACKEND 行为
import django.conf
if hasattr(django.conf.settings, 'EMAIL_BACKEND'):
print("WARNING: EMAIL_BACKEND is configured, should migrate to MAILERS")
# 2. 检查 on_delete 的使用情况
# 搜索所有 on_delete=CASCADE 的 ForeignKey,评估是否改为 DB_CASCADE
# 特别注意:依赖 pre_delete/post_delete 信号的
# 3. 检查密码哈希器配置
# 如果使用自定义哈希器,确认可以处理更高的迭代次数
# 4. 基准测试现有 QuerySet 的查询数量
# 使用 django.db.connection.queries 记录当前查询模式
5.2 推荐的升级路径
Django 6.0.x
↓
添加 MAILERS 配置(保留 EMAIL_BACKEND 兼容性)
↓
运行测试套件,验证无破坏性变更
↓
在 staging 环境启用 MAILERS,监控邮件发送
↓
评估 ForeignKey on_delete,识别可迁移到 DB_CASCADE 的字段
↓
Django 6.1.x(正式环境)
↓
运行 python manage.py migrate --fake-initial(如果需要)
↓
监控性能指标:查询数量、事务时间、内存使用
5.3 数据库迁移注意事项
对于 ForeignKey 迁移到 DB_CASCADE,需要注意数据库层面的约束添加:
-- PostgreSQL 示例:添加带 ON DELETE CASCADE 的外键约束
ALTER TABLE books_book
DROP CONSTRAINT books_book_author_id_fkey,
ADD CONSTRAINT books_book_author_id_fkey
FOREIGN KEY (author_id)
REFERENCES books_author(id)
ON DELETE CASCADE;
-- MySQL 示例
ALTER TABLE books_book
DROP FOREIGN KEY books_book_author_id_fkey,
ADD CONSTRAINT books_book_author_id_fkey
FOREIGN KEY (author_id)
REFERENCES books_author(id)
ON DELETE CASCADE;
-- SQLite 不支持外键约束修改,需要重建表
六、总结与展望:Django 的 2026 哲学
6.1 三大特性的共同主题
Django 6.1 的三大核心特性——Fetch Modes、DB_CASCADE、Mailers——有一个共同的主题:将更多的处理责任从应用层下沉到更底层的组件。
- Fetch Modes 把「预取策略」从显式声明变成运行时自动优化
- DB_CASCADE 把「级联删除」从 Python 循环变成数据库约束
- Mailers 把「邮件路由」从单一配置变成可编程的组件系统
这种设计哲学在 2026 年变得越来越主流。随着应用规模的扩大,开发者和框架作者都意识到:最好的优化不是让你写更多代码,而是让框架替你做正确的决定。
6.2 值得期待的未来版本
根据 Django 的开发路线图,以下是值得关注的趋势:
- Django 7.0:完全移除 EMAIL_BACKEND,MAILERS 成为唯一选择
- Django 7.x:可能引入 Async ORM 的进一步增强
- 长期:WebSocket 和 HTTP/2 的原生支持(目前需要通过 channels 实现)
6.3 给 Django 开发者的建议
- 立即行动:如果你的项目还在 Django 5.x,升级到 6.1 应该作为优先事项
- 性能审查:使用 Django Debug Toolbar 审计现有的 N+1 查询问题,评估 Fetch Modes 的适用场景
- 安全审计:检查密码哈希器配置,确保 PBKDF2 迭代次数符合 Django 6.1 标准
- 邮件迁移:虽然 Django 7.0 才会强制要求,但提前迁移到 MAILERS 可以避免未来紧急升级
Django 6.1 是一次厚积薄发的版本更新。它没有哗众取宠的新功能,但它在解决真实世界问题上走得非常扎实。如果你正在使用 Django,这是一个值得认真对待的升级。
标签:Django|Python|Web开发|ORM|性能优化|数据库|邮件系统|编程框架
关键字:Django 6.1|Fetch Modes|DB_CASCADE|MAILERS|N+1查询|ForeignKey|密码哈希|PBKDF2