🎯 目标读者 :Django 初学者 / 有一定 Python 基础但刚接触 Django 的同学
📖 阅读时长 :约 20~25 分钟(正文约 5 万字)
💡 特点:每一个知识点都配有详细解释 + 真实可运行的代码示例,力求"一看就会,一看就懂"
目录
[1. 写在前面:Django 是什么,为什么要学它](#1. 写在前面:Django 是什么,为什么要学它)
[1.1 Django 简介](#1.1 Django 简介)
[1.2 Django 的核心组件](#1.2 Django 的核心组件)
[1.3 快速搭建环境](#1.3 快速搭建环境)
[第一部分:Django ORM 深度解析](#第一部分:Django ORM 深度解析)
[2.1 ORM 的核心概念](#2.1 ORM 的核心概念)
[什么是 ORM?](#什么是 ORM?)
[ORM 的核心对应关系](#ORM 的核心对应关系)
[2.2 模型(Model)的定义与字段详解](#2.2 模型(Model)的定义与字段详解)
[2.2.1 定义第一个模型](#2.2.1 定义第一个模型)
[2.2.2 常用字段类型完全指南](#2.2.2 常用字段类型完全指南)
[JSON 字段(Django 3.1+)](#JSON 字段(Django 3.1+))
[2.2.3 字段通用参数](#2.2.3 字段通用参数)
[2.3 数据库迁移(Migration)原理与实践](#2.3 数据库迁移(Migration)原理与实践)
[2.3.1 迁移是什么](#2.3.1 迁移是什么)
[2.3.2 迁移工作流程](#2.3.2 迁移工作流程)
[2.3.3 迁移文件解析](#2.3.3 迁移文件解析)
[2.3.4 迁移操作详解](#2.3.4 迁移操作详解)
[2.3.5 常用迁移命令](#2.3.5 常用迁移命令)
[2.4 QuerySet 全面解析](#2.4 QuerySet 全面解析)
[2.4.1 QuerySet 是什么](#2.4.1 QuerySet 是什么)
[2.4.2 QuerySet 的缓存机制](#2.4.2 QuerySet 的缓存机制)
[2.5 增删改查(CRUD)操作大全](#2.5 增删改查(CRUD)操作大全)
[2.5.1 创建数据(Create)](#2.5.1 创建数据(Create))
[2.5.2 查询数据(Read)](#2.5.2 查询数据(Read))
[2.5.3 更新数据(Update)](#2.5.3 更新数据(Update))
[2.5.4 删除数据(Delete)](#2.5.4 删除数据(Delete))
[2.6 模型关系详解](#2.6 模型关系详解)
[2.6.1 一对多关系(ForeignKey)](#2.6.1 一对多关系(ForeignKey))
[2.6.2 多对多关系(ManyToManyField)](#2.6.2 多对多关系(ManyToManyField))
[2.6.3 一对一关系(OneToOneField)](#2.6.3 一对一关系(OneToOneField))
[2.6.4 自关联(Self-Referential)](#2.6.4 自关联(Self-Referential))
[2.7 聚合与注解](#2.7 聚合与注解)
[2.7.1 聚合(Aggregate)](#2.7.1 聚合(Aggregate))
[2.7.2 注解(Annotate)](#2.7.2 注解(Annotate))
[2.7.3 分组(values + annotate)](#2.7.3 分组(values + annotate))
[2.8 F 表达式与 Q 对象](#2.8 F 表达式与 Q 对象)
[2.8.1 F 表达式](#2.8.1 F 表达式)
[2.8.2 Q 对象](#2.8.2 Q 对象)
[2.9 数据库事务(Transaction)](#2.9 数据库事务(Transaction))
[2.9.1 为什么需要事务](#2.9.1 为什么需要事务)
[2.9.2 Django 中的事务控制](#2.9.2 Django 中的事务控制)
[2.10 原生 SQL 与 ORM 的混用](#2.10 原生 SQL 与 ORM 的混用)
[2.11 ORM 性能优化技巧](#2.11 ORM 性能优化技巧)
[2.11.1 常见的 N+1 问题](#2.11.1 常见的 N+1 问题)
[2.11.2 使用 Prefetch 对象精细化控制](#2.11.2 使用 Prefetch 对象精细化控制)
[2.11.3 只查询需要的字段](#2.11.3 只查询需要的字段)
[2.11.4 其他性能技巧](#2.11.4 其他性能技巧)
[2.12 自定义 Manager 与 QuerySet](#2.12 自定义 Manager 与 QuerySet)
[2.13 Meta 类详解](#2.13 Meta 类详解)
[第二部分:Django 中间件深度解析](#第二部分:Django 中间件深度解析)
[3.1 中间件是什么](#3.1 中间件是什么)
[3.1.1 用生活类比理解中间件](#3.1.1 用生活类比理解中间件)
[3.1.2 中间件的核心职责](#3.1.2 中间件的核心职责)
[3.2 请求/响应的生命周期](#3.2 请求/响应的生命周期)
[3.2.1 完整的生命周期图](#3.2.1 完整的生命周期图)
[3.2.2 中间件的四个钩子方法](#3.2.2 中间件的四个钩子方法)
[3.3 Django 内置中间件详解](#3.3 Django 内置中间件详解)
[CsrfViewMiddleware(CSRF 防护中间件)](#CsrfViewMiddleware(CSRF 防护中间件))
AuthenticationMiddleware(认证中间件)
[3.4 自定义中间件](#3.4 自定义中间件)
[3.4.1 中间件的两种写法](#3.4.1 中间件的两种写法)
[3.4.2 注册中间件](#3.4.2 注册中间件)
[3.5 中间件的执行顺序与陷阱](#3.5 中间件的执行顺序与陷阱)
[3.5.1 执行顺序](#3.5.1 执行顺序)
[3.5.2 顺序的重要性](#3.5.2 顺序的重要性)
[3.6 实战案例:各类中间件开发](#3.6 实战案例:各类中间件开发)
[案例2:IP 限流中间件](#案例2:IP 限流中间件)
[案例4:API 版本控制中间件](#案例4:API 版本控制中间件)
[第三部分:Django 信号深度解析](#第三部分:Django 信号深度解析)
[4.1 信号是什么,为什么需要它](#4.1 信号是什么,为什么需要它)
[4.1.1 信号的概念](#4.1.1 信号的概念)
[4.1.2 为什么需要信号](#4.1.2 为什么需要信号)
[4.2 Django 内置信号大全](#4.2 Django 内置信号大全)
[4.2.1 模型信号](#4.2.1 模型信号)
[4.2.2 请求/响应信号](#4.2.2 请求/响应信号)
[4.2.3 认证相关信号](#4.2.3 认证相关信号)
[4.2.4 数据库相关信号](#4.2.4 数据库相关信号)
[4.3 信号的连接方式](#4.3 信号的连接方式)
[4.3.1 使用 @receiver 装饰器(推荐)](#4.3.1 使用 @receiver 装饰器(推荐))
[4.3.2 使用 connect() 方法手动连接](#4.3.2 使用 connect() 方法手动连接)
[4.3.3 在 AppConfig 中注册信号(最佳实践)](#4.3.3 在 AppConfig 中注册信号(最佳实践))
[4.4 自定义信号](#4.4 自定义信号)
[4.5 信号的实战案例](#4.5 信号的实战案例)
[案例2:文章发布时自动生成 SEO 信息](#案例2:文章发布时自动生成 SEO 信息)
[4.6 信号的常见坑与最佳实践](#4.6 信号的常见坑与最佳实践)
[4.6.1 常见问题](#4.6.1 常见问题)
[问题2:信号处理中的数据库查询导致 N+1](#问题2:信号处理中的数据库查询导致 N+1)
[4.6.2 最佳实践总结](#4.6.2 最佳实践总结)
[5.1 博客系统完整实现](#5.1 博客系统完整实现)
[5.1.1 完整的模型设计](#5.1.1 完整的模型设计)
[5.1.2 视图层整合 ORM、中间件、信号](#5.1.2 视图层整合 ORM、中间件、信号)
[附录:常见问题 FAQ](#附录:常见问题 FAQ)
[Q1:ORM 生成的 SQL 是否会有性能问题?](#Q1:ORM 生成的 SQL 是否会有性能问题?)
[Q2:信号和 save() 方法覆盖,哪个更好?](#Q2:信号和 save() 方法覆盖,哪个更好?)
[Q5:Django ORM 支持哪些数据库?](#Q5:Django ORM 支持哪些数据库?)
[🗄️ Django ORM](#🗄️ Django ORM)
[🔗 Django 中间件](#🔗 Django 中间件)
[📡 Django 信号](#📡 Django 信号)
1. 写在前面:Django 是什么,为什么要学它
1.1 Django 简介
Django 是一个用 Python 编写的高级 Web 框架,诞生于 2003 年,2005 年开源。它的设计哲学是:
"Don't repeat yourself (DRY)" --- 不要重复自己
"Convention over configuration" --- 约定优于配置
简单来说,Django 帮你把开发 Web 应用时最繁琐的那些部分(数据库操作、URL 路由、表单验证、用户认证、后台管理......)都做好了,你只需要专注于业务逻辑。
1.2 Django 的核心组件
Django 遵循 MTV 架构(Model-Template-View),与大家熟知的 MVC 略有区别:
| 组件 | 对应 MVC | 职责 |
|---|---|---|
| Model(模型) | Model | 定义数据结构,与数据库交互 |
| Template(模板) | View | 定义页面的 HTML 展现 |
| View(视图) | Controller | 处理业务逻辑,连接 Model 和 Template |
本文重点讲的三个主题------ORM、中间件、信号------分别对应:
- ORM:Model 层的核心工具,让你用 Python 代码操作数据库
- 中间件:请求/响应处理的"流水线",可以在视图执行前后做各种处理
- 信号:组件之间的"广播系统",实现解耦通信
1.3 快速搭建环境
在正式开始之前,确保你的环境已经准备好:
# 创建虚拟环境
python -m venv django_env
# 激活虚拟环境(Windows)
django_env\Scripts\activate
# 激活虚拟环境(macOS/Linux)
source django_env/bin/activate
# 安装 Django
pip install django
# 验证安装
python -m django --version
# 输出类似:4.2.x 或 5.0.x
# 创建项目
django-admin startproject myproject
# 进入项目目录
cd myproject
# 创建应用
python manage.py startapp blog
# 运行开发服务器
python manage.py runserver
项目结构如下:
myproject/
├── manage.py # 项目管理脚本
├── myproject/
│ ├── __init__.py
│ ├── settings.py # 项目配置
│ ├── urls.py # URL 总路由
│ ├── asgi.py
│ └── wsgi.py
└── blog/
├── __init__.py
├── admin.py # 后台管理注册
├── apps.py # 应用配置
├── migrations/ # 数据库迁移文件
├── models.py # 数据模型
├── tests.py # 测试文件
└── views.py # 视图函数
第一部分:Django ORM 深度解析
2.1 ORM 的核心概念
什么是 ORM?
ORM(Object-Relational Mapping,对象关系映射) 是一种编程技术,它在关系型数据库 (表、行、列)和面向对象的编程语言(类、对象、属性)之间建立了一座桥梁。
用一个生活化的比喻来理解:
想象你去餐厅点餐,你不需要直接进厨房操作灶台(写 SQL),只需要告诉服务员(ORM)你想要什么,服务员会把你的需求翻译成厨房能理解的语言(SQL),然后把结果带回来给你(Python 对象)。
没有 ORM 时(直接写 SQL):
import sqlite3
conn = sqlite3.connect('mydb.db')
cursor = conn.cursor()
# 查询所有文章
cursor.execute("SELECT id, title, content, created_at FROM blog_post WHERE is_published = 1")
rows = cursor.fetchall()
for row in rows:
print(f"ID: {row[0]}, 标题: {row[1]}")
conn.close()
使用 Django ORM 后:
from blog.models import Post
# 查询所有已发布的文章
posts = Post.objects.filter(is_published=True)
for post in posts:
print(f"ID: {post.id}, 标题: {post.title}")
看出区别了吗?ORM 让代码:
- ✅ 更简洁易读
- ✅ 不用关心数据库语法差异(MySQL / PostgreSQL / SQLite 通用)
- ✅ 直接操作 Python 对象,更自然
- ✅ 自动防止 SQL 注入攻击
ORM 的核心对应关系
| 数据库概念 | Django ORM 概念 |
|---|---|
| 数据库表(Table) | 模型类(Model Class) |
| 表中的一行数据(Row) | 模型实例(Model Instance) |
| 列(Column) | 字段(Field) |
| SQL 查询语句 | QuerySet 方法 |
| 主键(Primary Key) | id 字段(自动创建) |
| 外键(Foreign Key) | ForeignKey 字段 |
2.2 模型(Model)的定义与字段详解
2.2.1 定义第一个模型
打开 blog/models.py,开始定义我们的博客系统模型:
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
class Category(models.Model):
"""文章分类"""
name = models.CharField(max_length=100, verbose_name='分类名称')
slug = models.SlugField(unique=True, verbose_name='URL别名')
description = models.TextField(blank=True, verbose_name='描述')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
class Meta:
verbose_name = '分类'
verbose_name_plural = '分类'
ordering = ['name']
def __str__(self):
return self.name
class Tag(models.Model):
"""文章标签"""
name = models.CharField(max_length=50, unique=True, verbose_name='标签名')
color = models.CharField(max_length=7, default='#007bff', verbose_name='颜色')
class Meta:
verbose_name = '标签'
verbose_name_plural = '标签'
def __str__(self):
return self.name
class Post(models.Model):
"""博客文章"""
STATUS_DRAFT = 'draft'
STATUS_PUBLISHED = 'published'
STATUS_CHOICES = [
(STATUS_DRAFT, '草稿'),
(STATUS_PUBLISHED, '已发布'),
]
title = models.CharField(max_length=200, verbose_name='标题')
slug = models.SlugField(max_length=200, unique_for_date='publish', verbose_name='URL别名')
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='blog_posts',
verbose_name='作者'
)
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='posts',
verbose_name='分类'
)
tags = models.ManyToManyField(
Tag,
blank=True,
related_name='posts',
verbose_name='标签'
)
body = models.TextField(verbose_name='正文')
summary = models.CharField(max_length=500, blank=True, verbose_name='摘要')
cover_image = models.ImageField(
upload_to='posts/%Y/%m/%d/',
blank=True,
null=True,
verbose_name='封面图片'
)
status = models.CharField(
max_length=10,
choices=STATUS_CHOICES,
default=STATUS_DRAFT,
verbose_name='状态'
)
publish = models.DateTimeField(default=timezone.now, verbose_name='发布时间')
created = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated = models.DateTimeField(auto_now=True, verbose_name='更新时间')
views = models.PositiveIntegerField(default=0, verbose_name='阅读量')
is_featured = models.BooleanField(default=False, verbose_name='是否精选')
class Meta:
verbose_name = '文章'
verbose_name_plural = '文章'
ordering = ['-publish']
indexes = [
models.Index(fields=['-publish']),
models.Index(fields=['status', '-publish']),
]
def __str__(self):
return self.title
def get_absolute_url(self):
from django.urls import reverse
return reverse('blog:post_detail', kwargs={
'year': self.publish.year,
'month': self.publish.month,
'day': self.publish.day,
'slug': self.slug
})
class Comment(models.Model):
"""文章评论"""
post = models.ForeignKey(
Post,
on_delete=models.CASCADE,
related_name='comments',
verbose_name='文章'
)
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='comments',
verbose_name='评论者'
)
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='replies',
verbose_name='父评论'
)
body = models.TextField(verbose_name='内容')
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
active = models.BooleanField(default=True, verbose_name='是否显示')
class Meta:
verbose_name = '评论'
verbose_name_plural = '评论'
ordering = ['created']
def __str__(self):
return f'{self.author.username} 评论了 {self.post.title}'
2.2.2 常用字段类型完全指南
Django 提供了非常丰富的字段类型。下面逐一讲解:
字符串相关字段
from django.db import models
class StringFieldDemo(models.Model):
# CharField:短字符串,必须指定 max_length
# 对应数据库:VARCHAR(max_length)
username = models.CharField(
max_length=150,
verbose_name='用户名',
help_text='用户名长度不超过150个字符'
)
# TextField:长文本,不限长度
# 对应数据库:TEXT 或 LONGTEXT
biography = models.TextField(
blank=True,
verbose_name='个人简介'
)
# EmailField:专门存储邮箱地址,内置格式验证
# 本质是 CharField(max_length=254)
email = models.EmailField(
unique=True,
verbose_name='邮箱'
)
# URLField:专门存储 URL,内置格式验证
website = models.URLField(
blank=True,
verbose_name='个人网站'
)
# SlugField:URL 友好的字符串(只含字母、数字、连字符、下划线)
# 常用于 SEO 友好的 URL
slug = models.SlugField(
max_length=200,
unique=True,
verbose_name='URL别名'
)
# UUIDField:存储 UUID,常用于分布式系统的主键
import uuid
uid = models.UUIDField(
default=uuid.uuid4,
editable=False,
unique=True,
verbose_name='唯一标识'
)
# IPAddressField / GenericIPAddressField:存储 IP 地址
ip_address = models.GenericIPAddressField(
null=True,
blank=True,
verbose_name='IP地址'
)
数字相关字段
class NumberFieldDemo(models.Model):
# IntegerField:整数,范围 -2147483648 到 2147483647
age = models.IntegerField(verbose_name='年龄')
# PositiveIntegerField:非负整数(0 到 2147483647)
views_count = models.PositiveIntegerField(default=0, verbose_name='浏览量')
# SmallIntegerField:小整数,范围 -32768 到 32767(节省空间)
rating = models.SmallIntegerField(verbose_name='评分')
# PositiveSmallIntegerField:非负小整数(0 到 32767)
priority = models.PositiveSmallIntegerField(default=0, verbose_name='优先级')
# BigIntegerField:大整数,-9223372036854775808 到 9223372036854775807
file_size = models.BigIntegerField(verbose_name='文件大小(字节)')
# FloatField:浮点数(注意:有精度问题,不适合存钱)
latitude = models.FloatField(verbose_name='纬度')
longitude = models.FloatField(verbose_name='经度')
# DecimalField:定点数,适合存储金额(不会有精度问题)
# max_digits:总位数;decimal_places:小数点后位数
price = models.DecimalField(
max_digits=10,
decimal_places=2,
verbose_name='价格'
) # 最大存储:99999999.99
# AutoField:自增整数主键(Django 默认的主键类型)
# BigAutoField:自增大整数主键(Django 3.2+ 默认)
# 通常不需要手动定义,Django 会自动创建 id 字段
日期和时间相关字段
class DateTimeFieldDemo(models.Model):
# DateField:只存储日期,格式:YYYY-MM-DD
birthday = models.DateField(
null=True,
blank=True,
verbose_name='生日'
)
# TimeField:只存储时间,格式:HH:MM[:ss[.uuuuuu]]
work_start_time = models.TimeField(verbose_name='上班时间')
# DateTimeField:存储日期+时间
# auto_now_add=True:创建时自动设置为当前时间,之后不变
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
# auto_now=True:每次保存时自动更新为当前时间
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
# 手动设置默认值
from django.utils import timezone
publish_at = models.DateTimeField(
default=timezone.now,
verbose_name='发布时间'
)
# DurationField:存储时间段(Python 的 timedelta 对象)
import datetime
session_duration = models.DurationField(
default=datetime.timedelta(hours=1),
verbose_name='会话时长'
)
# ⚠️ 重要区别:
# auto_now_add=True → 创建时自动填充,不可修改,等价于 editable=False
# auto_now=True → 每次 save() 时自动更新,不可修改
# default=timezone.now → 创建时自动填充,但可以手动修改
布尔和选择字段
class BoolChoiceFieldDemo(models.Model):
# BooleanField:True/False,对应数据库 TINYINT(1) 或 BOOLEAN
is_active = models.BooleanField(default=True, verbose_name='是否激活')
is_staff = models.BooleanField(default=False, verbose_name='是否员工')
# NullBooleanField(已废弃,用 BooleanField(null=True) 代替)
# 可以存储 True/False/None(数据库 NULL)
verified = models.BooleanField(null=True, blank=True, verbose_name='是否已验证')
# 使用 choices 实现枚举/选择字段
GENDER_MALE = 'M'
GENDER_FEMALE = 'F'
GENDER_OTHER = 'O'
GENDER_CHOICES = [
(GENDER_MALE, '男'),
(GENDER_FEMALE, '女'),
(GENDER_OTHER, '其他'),
]
gender = models.CharField(
max_length=1,
choices=GENDER_CHOICES,
blank=True,
verbose_name='性别'
)
# 也可以用整数作为选择值
LEVEL_BEGINNER = 1
LEVEL_INTERMEDIATE = 2
LEVEL_ADVANCED = 3
LEVEL_CHOICES = [
(LEVEL_BEGINNER, '初级'),
(LEVEL_INTERMEDIATE, '中级'),
(LEVEL_ADVANCED, '高级'),
]
level = models.IntegerField(
choices=LEVEL_CHOICES,
default=LEVEL_BEGINNER,
verbose_name='等级'
)
# 获取 choices 的显示值
def get_gender_display_custom(self):
return dict(self.GENDER_CHOICES).get(self.gender, '未知')
# Django 会自动生成 get_<field_name>_display() 方法
# self.get_gender_display() → '男' / '女' / '其他'
# self.get_level_display() → '初级' / '中级' / '高级'
文件相关字段
class FileFieldDemo(models.Model):
# FileField:上传任意文件
# upload_to 可以是字符串(目录路径)或可调用对象
document = models.FileField(
upload_to='documents/',
blank=True,
null=True,
verbose_name='文档'
)
# 使用函数动态生成上传路径
def user_directory_path(instance, filename):
# 文件将被上传到 MEDIA_ROOT/user_<id>/<filename>
return f'user_{instance.user.id}/{filename}'
# ImageField:专门用于图片,继承自 FileField
# 需要安装 Pillow 库:pip install Pillow
avatar = models.ImageField(
upload_to='avatars/%Y/%m/', # 按年月分目录
blank=True,
null=True,
width_field='avatar_width', # 自动记录宽度(可选)
height_field='avatar_height', # 自动记录高度(可选)
verbose_name='头像'
)
avatar_width = models.PositiveIntegerField(null=True, blank=True)
avatar_height = models.PositiveIntegerField(null=True, blank=True)
# 访问文件 URL:instance.document.url
# 访问文件名:instance.document.name
JSON 字段(Django 3.1+)
class JSONFieldDemo(models.Model):
# JSONField:存储 JSON 数据(PostgreSQL、MySQL 5.7+、SQLite 3.9+)
metadata = models.JSONField(
default=dict, # 默认为空字典
blank=True,
verbose_name='元数据'
)
tags = models.JSONField(
default=list, # 默认为空列表
blank=True,
verbose_name='标签列表'
)
config = models.JSONField(
default=dict,
verbose_name='配置信息'
)
# 使用示例
# obj.metadata = {"views": 100, "likes": 50}
# obj.tags = ["python", "django", "orm"]
# obj.save()
# 查询 JSON 字段(仅 PostgreSQL 完整支持)
# Post.objects.filter(metadata__views__gt=50)
# Post.objects.filter(tags__contains=["python"])
2.2.3 字段通用参数
每个字段都支持以下通用参数:
class FieldOptionsDemo(models.Model):
# null=True:数据库中允许 NULL 值
# blank=True:表单验证中允许为空(注意与 null 的区别!)
# 字符串字段建议只用 blank=True,不用 null=True(避免两种空值)
# 非字符串字段(数字、日期等)需要 null=True 才能存空值
phone = models.CharField(
max_length=20,
blank=True, # 表单可以不填
default='', # 数据库存空字符串,不存 NULL
verbose_name='电话'
)
score = models.FloatField(
null=True, # 数据库允许 NULL
blank=True, # 表单可以不填
verbose_name='分数'
)
# default:字段的默认值,可以是值或可调用对象
import uuid
order_no = models.CharField(
max_length=32,
default=lambda: uuid.uuid4().hex, # 可调用对象
verbose_name='订单号'
)
# unique=True:该字段值在整张表中必须唯一
email = models.EmailField(unique=True, verbose_name='邮箱')
# db_index=True:为该字段创建数据库索引,加速查询
username = models.CharField(max_length=150, db_index=True, verbose_name='用户名')
# verbose_name:字段的人类可读名称(在 Admin 后台等处显示)
# help_text:字段的帮助文本
bio = models.TextField(
blank=True,
verbose_name='个人简介',
help_text='请用不超过500字介绍自己'
)
# editable=False:该字段不显示在表单和 Admin 中
created_at = models.DateTimeField(auto_now_add=True, editable=False)
# primary_key=True:将该字段设为主键(代替自动创建的 id 字段)
# 注意:一旦设置,Django 不再自动创建 id 字段
# custom_id = models.CharField(max_length=32, primary_key=True)
# choices:限制字段的可选值
# db_column:指定数据库中的列名(默认使用字段名)
# db_tablespace:指定索引的表空间(高级用法)
STATUS_CHOICES = [('active', '活跃'), ('inactive', '非活跃')]
status = models.CharField(
max_length=10,
choices=STATUS_CHOICES,
default='active',
db_column='user_status', # 数据库列名为 user_status
verbose_name='状态'
)
2.3 数据库迁移(Migration)原理与实践
2.3.1 迁移是什么
迁移(Migration) 是 Django 跟踪模型变化并同步到数据库的机制。你可以把它理解为数据库的版本控制------就像 Git 记录代码变化一样,迁移记录了数据库结构的变化历史。
2.3.2 迁移工作流程
# 第一步:修改 models.py 中的模型
# 第二步:生成迁移文件(检测模型变化,生成 Python 迁移脚本)
python manage.py makemigrations
# 也可以为特定应用生成迁移
python manage.py makemigrations blog
# 第三步:查看迁移会生成什么 SQL(可选,用于检查)
python manage.py sqlmigrate blog 0001
# 第四步:执行迁移(将变更应用到数据库)
python manage.py migrate
# 查看所有迁移状态([X] 表示已执行,[ ] 表示未执行)
python manage.py showmigrations
2.3.3 迁移文件解析
执行 makemigrations 后,会在 blog/migrations/ 目录生成类似这样的文件:
# blog/migrations/0001_initial.py
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
"""
这是 blog 应用的第一次迁移,创建初始表结构
"""
initial = True
dependencies = [
# 依赖其他应用的迁移(因为我们用了 User 外键)
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
# 创建 Category 表
migrations.CreateModel(
name='Category',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True)),
('name', models.CharField(max_length=100, verbose_name='分类名称')),
('slug', models.SlugField(unique=True, verbose_name='URL别名')),
('description', models.TextField(blank=True, verbose_name='描述')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
],
options={
'verbose_name': '分类',
'verbose_name_plural': '分类',
'ordering': ['name'],
},
),
# 创建 Post 表...
migrations.CreateModel(
name='Post',
fields=[
# ... 字段列表
],
),
]
2.3.4 迁移操作详解
当你修改模型时,Django 会生成对应的迁移操作:
# 迁移操作类型示例
# 1. 添加字段(AddField)
migrations.AddField(
model_name='post',
name='like_count',
field=models.PositiveIntegerField(default=0),
),
# 2. 删除字段(RemoveField)
migrations.RemoveField(
model_name='post',
name='old_field',
),
# 3. 修改字段(AlterField)
migrations.AlterField(
model_name='post',
name='title',
field=models.CharField(max_length=300, verbose_name='标题'), # 从 200 改为 300
),
# 4. 重命名字段(RenameField)
migrations.RenameField(
model_name='post',
old_name='content',
new_name='body',
),
# 5. 重命名模型(RenameModel)
migrations.RenameModel(
old_name='Post',
new_name='Article',
),
# 6. 删除模型(DeleteModel)
migrations.DeleteModel(
name='OldModel',
),
# 7. 添加索引(AddIndex)
migrations.AddIndex(
model_name='post',
index=models.Index(fields=['-publish'], name='post_publish_idx'),
),
# 8. 数据迁移(RunPython)--- 用于修改已有数据
def update_existing_posts(apps, schema_editor):
Post = apps.get_model('blog', 'Post')
for post in Post.objects.all():
post.summary = post.body[:200] # 用正文前200字填充摘要
post.save()
migrations.RunPython(
update_existing_posts,
reverse_code=migrations.RunPython.noop, # 回滚时什么都不做
),
2.3.5 常用迁移命令
# 回滚到指定迁移(数字为迁移编号)
python manage.py migrate blog 0001
# 回滚该应用的所有迁移(慎用!会删除数据)
python manage.py migrate blog zero
# 生成迁移但不写入文件(查看会生成什么迁移)
python manage.py makemigrations --dry-run
# 合并迁移(当多个分支的迁移产生冲突时)
python manage.py squashmigrations blog 0001 0005
# 伪造迁移(告诉 Django 某个迁移已执行,但不实际执行)
# 场景:手动修改了数据库结构,需要同步 Django 的迁移状态
python manage.py migrate blog 0001 --fake
# 从现有数据库表生成初始迁移(避免重复建表)
python manage.py migrate --fake-initial
2.4 QuerySet 全面解析
2.4.1 QuerySet 是什么
QuerySet(查询集) 是 Django ORM 中最重要的概念。它代表了一组从数据库中获取的对象集合。
QuerySet 有两个非常重要的特性:
-
惰性求值(Lazy Evaluation):QuerySet 被创建时不会立即访问数据库,只有在真正需要数据时才会执行 SQL 查询。
-
可链式调用(Chainable):大多数 QuerySet 方法返回一个新的 QuerySet,可以不断链式调用。
from blog.models import Post
这行代码不会访问数据库!只是创建了一个 QuerySet 对象
posts = Post.objects.filter(status='published')
这里也不会访问数据库,只是在之前的基础上加了条件
recent_posts = posts.order_by('-publish')
直到这里,才真正执行 SQL 查询
for post in recent_posts:
print(post.title) # 迭代时触发数据库查询其他触发数据库查询的操作:
count = recent_posts.count() # 触发查询
first = recent_posts.first() # 触发查询
exists = recent_posts.exists() # 触发查询
list(recent_posts) # 触发查询
recent_posts[0] # 触发查询(切片操作)
2.4.2 QuerySet 的缓存机制
# 理解缓存对性能的影响
# ❌ 低效:两次数据库查询
posts = Post.objects.filter(status='published')
if posts: # 第一次查询
for post in posts: # 第二次查询
print(post.title)
# ✅ 高效:只查询一次(结果已缓存)
posts = Post.objects.filter(status='published')
posts_list = list(posts) # 立即执行查询并缓存结果
if posts_list:
for post in posts_list:
print(post.title) # 从缓存读取,不再查数据库
# 注意:切片操作不会产生完整的缓存
posts = Post.objects.all()
posts[0] # 查询一次
posts[0] # 又查询一次!切片不缓存
2.5 增删改查(CRUD)操作大全
2.5.1 创建数据(Create)
from django.contrib.auth.models import User
from blog.models import Post, Category, Tag
# 方法一:先创建实例,再保存(最常用)
category = Category(name='技术', slug='tech', description='技术相关文章')
category.save() # 这时才真正写入数据库
# 方法二:直接使用 create()(创建并立即保存,一步完成)
category = Category.objects.create(
name='生活',
slug='life',
description='生活日常'
)
# 方法三:get_or_create()------存在则获取,不存在则创建
# 返回 (对象, 是否新创建的布尔值)
category, created = Category.objects.get_or_create(
slug='tech', # 查找条件
defaults={ # 如果不存在,用这些值创建
'name': '技术',
'description': '技术文章'
}
)
if created:
print(f"创建了新分类:{category.name}")
else:
print(f"找到了已有分类:{category.name}")
# 方法四:update_or_create()------存在则更新,不存在则创建
category, created = Category.objects.update_or_create(
slug='tech', # 查找条件
defaults={ # 如果存在则更新这些字段,不存在则创建
'name': '技术与编程',
'description': '技术文章,包含编程、运维等'
}
)
# 方法五:bulk_create()------批量创建(一次 SQL,效率高)
categories = [
Category(name='前端', slug='frontend'),
Category(name='后端', slug='backend'),
Category(name='数据库', slug='database'),
Category(name='运维', slug='devops'),
]
Category.objects.bulk_create(categories)
# bulk_create 的高级用法
# ignore_conflicts=True:忽略唯一约束冲突
Category.objects.bulk_create(
categories,
ignore_conflicts=True
)
# batch_size:控制每批插入的数量(数据量大时避免单次SQL过大)
Category.objects.bulk_create(
categories,
batch_size=100 # 每次插入100条
)
# 创建文章(包含外键)
user = User.objects.get(username='admin')
post = Post.objects.create(
title='Django ORM 完全指南',
slug='django-orm-complete-guide',
author=user,
category=category,
body='这是文章正文内容...',
status='published'
)
# 添加多对多关系(tag)
tag1 = Tag.objects.create(name='Django', color='#FF5722')
tag2 = Tag.objects.create(name='Python', color='#2196F3')
post.tags.add(tag1, tag2) # 添加标签
post.tags.remove(tag1) # 删除某个标签
post.tags.set([tag1, tag2]) # 替换所有标签
post.tags.clear() # 清除所有标签
2.5.2 查询数据(Read)
# ============ 基础查询 ============
# 获取所有对象(返回 QuerySet)
all_posts = Post.objects.all()
# 获取单个对象(不存在抛 DoesNotExist,多个抛 MultipleObjectsReturned)
post = Post.objects.get(id=1)
post = Post.objects.get(pk=1) # pk 等价于 id
# 安全获取单个对象(不存在返回 None,不抛异常)
post = Post.objects.filter(id=1).first() # 返回第一个或 None
# ============ 过滤查询 ============
# filter():过滤,返回满足条件的 QuerySet
published_posts = Post.objects.filter(status='published')
# exclude():排除,返回不满足条件的 QuerySet
non_draft_posts = Post.objects.exclude(status='draft')
# 多条件过滤(AND 关系)
posts = Post.objects.filter(
status='published',
is_featured=True
)
# 链式调用(等价于上面的多条件过滤)
posts = Post.objects.filter(status='published').filter(is_featured=True)
# ============ 查找类型(Field Lookups)============
# 精确匹配(=)
Post.objects.filter(id=1)
Post.objects.filter(id__exact=1) # 等价
# 大小写不敏感的精确匹配
Post.objects.filter(title__iexact='django orm')
# 包含(LIKE '%value%')
Post.objects.filter(title__contains='Django') # 区分大小写
Post.objects.filter(title__icontains='django') # 不区分大小写
# 开头/结尾匹配
Post.objects.filter(title__startswith='Django')
Post.objects.filter(title__istartswith='django') # 不区分大小写
Post.objects.filter(title__endswith='指南')
# 范围查询
Post.objects.filter(views__gt=100) # 大于 100
Post.objects.filter(views__gte=100) # 大于等于 100
Post.objects.filter(views__lt=1000) # 小于 1000
Post.objects.filter(views__lte=1000) # 小于等于 1000
Post.objects.filter(views__range=(100, 1000)) # 100 到 1000 之间(含)
# 日期相关查询
from django.utils import timezone
import datetime
# 查询今天发布的文章
today = timezone.now().date()
Post.objects.filter(publish__date=today)
# 查询某年的文章
Post.objects.filter(publish__year=2024)
# 查询某个月的文章
Post.objects.filter(publish__month=12)
# 查询某天
Post.objects.filter(publish__day=25)
# 查询某个时间范围(最近7天)
week_ago = timezone.now() - datetime.timedelta(days=7)
Post.objects.filter(publish__gte=week_ago)
# 查询某个时间的小时
Post.objects.filter(publish__hour=10)
# IN 查询(类似 SQL 的 WHERE id IN (1, 2, 3))
Post.objects.filter(id__in=[1, 2, 3])
# ISNULL 查询
Post.objects.filter(cover_image__isnull=True) # cover_image 为 NULL
Post.objects.filter(cover_image__isnull=False) # cover_image 不为 NULL
# 跨关联查询(通过双下划线跨越外键)
# 查询作者用户名为 'admin' 的文章
Post.objects.filter(author__username='admin')
# 跨多层关联
Post.objects.filter(author__profile__city='北京')
# 查询有特定分类名称的文章
Post.objects.filter(category__name='技术')
# 反向查询(通过相关模型过滤)
# 查询有评论的文章(通过 related_name='comments')
Post.objects.filter(comments__isnull=False).distinct()
# ============ 排序 ============
# 按某个字段升序
Post.objects.order_by('publish')
# 按某个字段降序(加 - 号)
Post.objects.order_by('-publish')
# 多字段排序(先按第一个,再按第二个)
Post.objects.order_by('category', '-publish')
# 按随机顺序
Post.objects.order_by('?') # 注意:性能较差
# 按关联字段排序
Post.objects.order_by('author__username', '-publish')
# 清除排序
Post.objects.all().order_by() # 移除 Meta 中的默认排序
# ============ 切片和分页 ============
# 切片(对应 SQL 的 LIMIT 和 OFFSET)
first_5 = Post.objects.all()[:5] # 前5条(LIMIT 5)
next_5 = Post.objects.all()[5:10] # 第6到10条(LIMIT 5 OFFSET 5)
last = Post.objects.all().order_by('-id')[0] # 最新一条
# 注意:不支持负索引!
# Post.objects.all()[-1] # ❌ 报错!
# 分页器(Paginator)--- 推荐用于实际分页
from django.core.paginator import Paginator
posts = Post.objects.filter(status='published').order_by('-publish')
paginator = Paginator(posts, 10) # 每页10条
page_1 = paginator.page(1) # 第1页
print(page_1.object_list) # 本页的对象列表
print(page_1.has_next()) # 是否有下一页
print(page_1.has_previous()) # 是否有上一页
print(page_1.next_page_number()) # 下一页页码
print(paginator.num_pages) # 总页数
print(paginator.count) # 总条数
# ============ 去重 ============
# distinct():去除重复结果(通常与跨关联查询一起使用)
posts_with_comments = Post.objects.filter(
comments__isnull=False
).distinct() # 避免同一篇文章因多个评论重复出现
# ============ 值查询 ============
# values():返回字典列表(只包含指定字段)
Post.objects.values('id', 'title', 'publish')
# 返回:<QuerySet [{'id': 1, 'title': '...', 'publish': ...}, ...]>
# values_list():返回元组列表
Post.objects.values_list('id', 'title')
# 返回:<QuerySet [(1, '...'), (2, '...'), ...]>
# flat=True:只有一个字段时,直接返回值列表(不是元组)
Post.objects.values_list('title', flat=True)
# 返回:<QuerySet ['标题1', '标题2', ...]>
# named=True:返回具名元组
posts = Post.objects.values_list('id', 'title', named=True)
for p in posts:
print(p.id, p.title) # 可以用属性访问
# ============ 其他实用查询 ============
# count():计数(比 len() 效率高,直接执行 COUNT SQL)
total = Post.objects.count()
published_count = Post.objects.filter(status='published').count()
# exists():判断是否存在(比 count() > 0 效率高)
if Post.objects.filter(slug='my-post').exists():
print("文章已存在")
# first() / last():获取第一/最后一个(不存在返回 None)
newest = Post.objects.order_by('-publish').first()
oldest = Post.objects.order_by('publish').first()
# latest() / earliest():基于日期字段获取最新/最早(可能抛异常)
newest = Post.objects.latest('publish') # 等价于 order_by('-publish').first()
oldest = Post.objects.earliest('publish') # 等价于 order_by('publish').first()
# contains()(Django 4.0+):检查 QuerySet 是否包含某个对象
post = Post.objects.get(id=1)
if Post.objects.filter(status='published').contains(post):
print("该文章已发布")
2.5.3 更新数据(Update)
# 方法一:修改实例属性后调用 save()
post = Post.objects.get(id=1)
post.title = '新标题'
post.views = post.views + 1 # ⚠️ 有并发问题!
post.save()
# 只更新指定字段(性能更好,避免全字段更新)
post.title = '新标题'
post.save(update_fields=['title', 'updated']) # 只更新这两个字段
# 方法二:QuerySet.update()------批量更新(一条 SQL,效率最高)
# 将所有草稿文章的状态改为已发布
Post.objects.filter(status='draft').update(status='published')
# 可以同时更新多个字段
from django.utils import timezone
Post.objects.filter(id__in=[1, 2, 3]).update(
is_featured=True,
updated=timezone.now()
)
# 使用 F 表达式做原子性更新(避免并发问题)
from django.db.models import F
# ❌ 有并发问题的写法(先读后写,并发时可能丢失更新)
post = Post.objects.get(id=1)
post.views = post.views + 1
post.save()
# ✅ 原子性更新(直接在数据库层面做加法)
Post.objects.filter(id=1).update(views=F('views') + 1)
# bulk_update()------批量更新多个对象(Django 2.2+)
posts = list(Post.objects.filter(status='draft'))
for post in posts:
post.status = 'published'
post.views = 0
Post.objects.bulk_update(posts, ['status', 'views']) # 指定要更新的字段
2.5.4 删除数据(Delete)
# 方法一:删除单个实例
post = Post.objects.get(id=1)
post.delete() # 返回 (删除数量, {模型名: 数量}) 的元组
# 方法二:QuerySet.delete()------批量删除
# 删除所有草稿文章
deleted_count, details = Post.objects.filter(status='draft').delete()
print(f"删除了 {deleted_count} 条记录")
print(details) # {'blog.Post': 5, 'blog.Comment': 23}(级联删除的数量)
# 注意:delete() 不会直接在 QuerySet 上操作,而是先获取所有对象,再逐一删除
# 这是为了确保每个对象的 delete() 方法都被调用(触发信号等)
# on_delete 参数控制级联行为:
# CASCADE:级联删除(删除父记录时,子记录也删除)------ 最常用
# SET_NULL:父记录删除时,子记录的外键置为 NULL(需要字段允许 null)
# SET_DEFAULT:父记录删除时,子记录的外键置为默认值
# PROTECT:父记录有子记录时,阻止删除(抛出 ProtectedError)
# RESTRICT:类似 PROTECT,但有细微区别
# DO_NOTHING:父记录删除时,不对子记录做任何处理(可能导致数据库报错)
# 示例:理解 on_delete 的影响
# 如果一篇文章有评论,且评论的 on_delete=CASCADE:
# 删除文章 → 该文章的所有评论也被自动删除
post = Post.objects.get(id=1)
post.delete() # 文章和它的所有评论都被删除
# 软删除(推荐的生产环境做法)
# 不实际删除,只标记为已删除
class SoftDeleteModel(models.Model):
is_deleted = models.BooleanField(default=False)
deleted_at = models.DateTimeField(null=True, blank=True)
def soft_delete(self):
self.is_deleted = True
self.deleted_at = timezone.now()
self.save(update_fields=['is_deleted', 'deleted_at'])
class Meta:
abstract = True # 抽象模型,不会创建数据库表
2.6 模型关系详解
2.6.1 一对多关系(ForeignKey)
一对多是最常见的关系。比如:一个作者可以写多篇文章,一篇文章只能有一个作者。
from django.db import models
from django.contrib.auth.models import User
class Post(models.Model):
# ForeignKey 定义多对一(即从文章角度看是多对一,从作者角度看是一对多)
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='posts', # 反向查询名称:user.posts.all()
verbose_name='作者'
)
title = models.CharField(max_length=200)
body = models.TextField()
# ============ 正向查询(从文章查作者)============
post = Post.objects.get(id=1)
# 访问外键对象(会产生一次数据库查询)
author = post.author # 返回 User 对象
print(author.username)
# 只获取外键 ID(不产生额外查询,直接读取缓存)
author_id = post.author_id # 返回整数,不是对象!
author_id = post.author_pk # 同上(不太常用)
# ============ 反向查询(从作者查文章)============
user = User.objects.get(username='admin')
# 通过 related_name 访问反向关系
posts = user.posts.all() # 该用户的所有文章
published = user.posts.filter(status='published')
post_count = user.posts.count()
latest_post = user.posts.latest('publish')
# 如果没有定义 related_name,Django 自动生成 <模型名小写>_set
# 例如:user.post_set.all()(不推荐,不如手动指定 related_name)
# ============ 外键的 select_related(性能优化)============
# 问题:N+1 查询
posts = Post.objects.all()
for post in posts:
print(post.author.username) # 每次循环都查一次 User!共 N+1 次查询
# 解决:使用 select_related 预先加载外键关联数据(JOIN 查询)
posts = Post.objects.select_related('author', 'category').all()
for post in posts:
print(post.author.username) # 不再额外查询!
print(post.category.name) # 同上
# select_related 适用于 ForeignKey 和 OneToOneField(单对象关联)
2.6.2 多对多关系(ManyToManyField)
多对多关系中间会自动创建一张关联表。比如:一篇文章可以有多个标签,一个标签也可以对应多篇文章。
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
class Post(models.Model):
tags = models.ManyToManyField(
Tag,
blank=True,
related_name='posts',
verbose_name='标签'
)
# ============ 多对多操作 ============
post = Post.objects.get(id=1)
tag_python = Tag.objects.get(name='Python')
tag_django = Tag.objects.get(name='Django')
# 添加关联
post.tags.add(tag_python) # 添加一个
post.tags.add(tag_python, tag_django) # 同时添加多个
post.tags.add(*Tag.objects.filter(name__in=['Python', 'Django'])) # 添加 QuerySet
# 移除关联
post.tags.remove(tag_python)
# 设置(替换所有关联)
post.tags.set([tag_python, tag_django]) # 先清除再添加
post.tags.set([tag_python]) # 现在只有 Python 标签了
# 清除所有关联
post.tags.clear()
# 查询
all_tags = post.tags.all()
tag_count = post.tags.count()
# 反向查询:通过标签查文章
tag = Tag.objects.get(name='Python')
python_posts = tag.posts.all()
# 判断是否存在关联
if tag in post.tags.all():
print("该文章有 Python 标签")
# ============ 中间表(through 参数)============
# 当多对多关系需要存储额外字段时,使用自定义中间表
class PostTagRelation(models.Model):
"""文章与标签的关系表(存储额外信息)"""
post = models.ForeignKey('Post', on_delete=models.CASCADE)
tag = models.ForeignKey('Tag', on_delete=models.CASCADE)
added_at = models.DateTimeField(auto_now_add=True) # 添加时间
added_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) # 谁添加的
class Meta:
unique_together = ['post', 'tag'] # 确保不重复添加
class Post(models.Model):
tags = models.ManyToManyField(
Tag,
through='PostTagRelation', # 指定中间表
related_name='posts'
)
# 使用自定义中间表时,不能直接用 add/remove/set
# 需要直接操作中间表
PostTagRelation.objects.create(
post=post,
tag=tag_python,
added_by=user
)
# ============ prefetch_related(多对多性能优化)============
# 问题:N+1 查询
posts = Post.objects.all()
for post in posts:
for tag in post.tags.all(): # 每次循环都查一次标签!
print(tag.name)
# 解决:使用 prefetch_related(发两个 SQL:一个查文章,一个查所有标签)
posts = Post.objects.prefetch_related('tags').all()
for post in posts:
for tag in post.tags.all(): # 从缓存读取,不再查数据库
print(tag.name)
# 同时使用 select_related 和 prefetch_related
posts = Post.objects.select_related('author', 'category').prefetch_related('tags').all()
2.6.3 一对一关系(OneToOneField)
一对一关系用于模型扩展。比如:一个用户有且只有一个个人资料。
class UserProfile(models.Model):
"""用户扩展资料(对 User 模型的扩展)"""
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name='profile',
verbose_name='用户'
)
bio = models.TextField(blank=True, verbose_name='个人简介')
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
website = models.URLField(blank=True)
city = models.CharField(max_length=100, blank=True)
birth_date = models.DateField(null=True, blank=True)
def __str__(self):
return f'{self.user.username} 的资料'
# ============ 一对一查询 ============
# 正向查询
user = User.objects.get(username='admin')
profile = user.profile # 返回 UserProfile 对象
print(profile.bio)
# 反向查询
profile = UserProfile.objects.get(id=1)
user = profile.user
# 如果 profile 不存在,访问 user.profile 会抛出 RelatedObjectDoesNotExist
# 安全写法:
try:
profile = user.profile
except UserProfile.DoesNotExist:
profile = UserProfile.objects.create(user=user)
# 更推荐的写法:
profile, created = UserProfile.objects.get_or_create(user=user)
# ============ 使用 select_related 优化一对一查询 ============
user = User.objects.select_related('profile').get(username='admin')
print(user.profile.bio) # 不产生额外查询
2.6.4 自关联(Self-Referential)
模型可以与自身建立关系,常用于树形结构(菜单、评论回复、分类等):
class Comment(models.Model):
"""支持嵌套回复的评论"""
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
author = models.ForeignKey(User, on_delete=models.CASCADE)
parent = models.ForeignKey(
'self', # 'self' 表示指向自身
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='replies',
verbose_name='父评论'
)
body = models.TextField()
created = models.DateTimeField(auto_now_add=True)
class Category(models.Model):
"""支持多级分类的分类模型"""
name = models.CharField(max_length=100)
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='children',
verbose_name='父分类'
)
def get_ancestors(self):
"""获取所有祖先分类"""
ancestors = []
parent = self.parent
while parent:
ancestors.insert(0, parent)
parent = parent.parent
return ancestors
def get_descendants(self):
"""获取所有子孙分类"""
descendants = []
children = self.children.all()
for child in children:
descendants.append(child)
descendants.extend(child.get_descendants())
return descendants
# 使用示例
# 创建分类树
tech = Category.objects.create(name='技术', parent=None)
backend = Category.objects.create(name='后端', parent=tech)
python = Category.objects.create(name='Python', parent=backend)
django = Category.objects.create(name='Django', parent=python)
# 查询
print(django.parent.name) # Python
print(python.parent.name) # 后端
print(tech.children.all()) # [后端]
print(python.get_ancestors()) # [技术, 后端]
2.7 聚合与注解
2.7.1 聚合(Aggregate)
聚合函数对整个 QuerySet 做统计计算,返回一个字典。
from django.db.models import (
Count, Sum, Avg, Max, Min,
StdDev, Variance
)
from blog.models import Post, Comment
# ============ 基础聚合 ============
# Count:计数
result = Post.objects.aggregate(Count('id'))
# 返回:{'id__count': 42}
# 自定义键名(更清晰)
result = Post.objects.aggregate(total=Count('id'))
# 返回:{'total': 42}
# 多个聚合同时计算
result = Post.objects.aggregate(
total=Count('id'),
max_views=Max('views'),
min_views=Min('views'),
avg_views=Avg('views'),
total_views=Sum('views'),
)
# 返回:{'total': 42, 'max_views': 9999, 'min_views': 0, 'avg_views': 234.5, 'total_views': 9849}
# 对过滤后的 QuerySet 做聚合
result = Post.objects.filter(status='published').aggregate(
published_count=Count('id'),
avg_views=Avg('views'),
)
# Count 去重
result = Post.objects.aggregate(
unique_authors=Count('author', distinct=True) # 去重后计数
)
# 跨关联字段聚合
result = Post.objects.aggregate(
total_comments=Count('comments') # 所有文章的评论总数
)
2.7.2 注解(Annotate)
注解为 QuerySet 中的每个对象添加聚合字段,返回增强后的 QuerySet。
from django.db.models import Count, Avg, F, Value
from django.db.models.functions import Concat
# ============ 基础注解 ============
# 为每篇文章注解评论数量
posts = Post.objects.annotate(
comment_count=Count('comments')
)
for post in posts:
print(f"{post.title}: {post.comment_count} 条评论")
# 注解后可以排序
posts = Post.objects.annotate(
comment_count=Count('comments')
).order_by('-comment_count') # 按评论数量降序
# 注解后可以过滤
posts = Post.objects.annotate(
comment_count=Count('comments')
).filter(comment_count__gt=5) # 只要评论超过5条的
# ============ 高级注解 ============
from django.db.models import Case, When, IntegerField
# 条件注解(CASE WHEN)
posts = Post.objects.annotate(
popularity_level=Case(
When(views__lt=100, then=Value('低')),
When(views__lt=1000, then=Value('中')),
When(views__gte=1000, then=Value('高')),
default=Value('未知'),
output_field=models.CharField(),
)
)
for post in posts:
print(f"{post.title}: 热度 {post.popularity_level}")
# 字符串拼接注解
from django.db.models.functions import Concat
from django.db.models import CharField
posts = Post.objects.annotate(
full_info=Concat(
'title', Value(' by '), 'author__username',
output_field=CharField()
)
)
# 日期提取注解
from django.db.models.functions import TruncMonth, ExtractYear
posts = Post.objects.annotate(
publish_month=TruncMonth('publish')
).values('publish_month').annotate(
count=Count('id')
).order_by('publish_month')
# 按月统计文章数量
# 多层注解
posts = Post.objects.annotate(
comment_count=Count('comments', distinct=True),
active_comment_count=Count(
'comments',
filter=models.Q(comments__active=True),
distinct=True
),
avg_reply_count=Avg(
Count('comments__replies', distinct=True)
)
)
2.7.3 分组(values + annotate)
from django.db.models import Count, Avg
# 按作者分组,统计每位作者的文章数量
author_stats = Post.objects.values('author__username').annotate(
post_count=Count('id')
).order_by('-post_count')
for stat in author_stats:
print(f"作者: {stat['author__username']}, 文章数: {stat['post_count']}")
# 按状态分组统计
status_stats = Post.objects.values('status').annotate(
count=Count('id'),
avg_views=Avg('views')
)
# 按发布年月分组统计(类似 SQL 的 GROUP BY)
from django.db.models.functions import TruncMonth
monthly_stats = Post.objects.filter(
status='published'
).annotate(
month=TruncMonth('publish')
).values('month').annotate(
count=Count('id'),
total_views=Sum('views')
).order_by('-month')
for stat in monthly_stats:
print(f"{stat['month'].strftime('%Y-%m')}: {stat['count']} 篇, 共 {stat['total_views']} 次浏览")
# HAVING 子句(过滤分组结果)
# 找出文章数量超过5篇的作者
active_authors = Post.objects.values('author').annotate(
post_count=Count('id')
).filter(
post_count__gt=5 # 这里的 filter 等价于 SQL 的 HAVING
).order_by('-post_count')
2.8 F 表达式与 Q 对象
2.8.1 F 表达式
F 表达式引用模型字段的值,用于字段间的比较和原子性更新。
from django.db.models import F
# ============ 原子性更新(最常用!)============
# ❌ 非原子更新(并发时有问题)
post = Post.objects.get(id=1)
post.views = post.views + 1
post.save()
# 问题:先 SELECT,再 UPDATE,中间可能有其他请求也做了更新!
# ✅ 原子更新(直接在数据库层面完成)
Post.objects.filter(id=1).update(views=F('views') + 1)
# 等价 SQL:UPDATE blog_post SET views = views + 1 WHERE id = 1
# 也可以在 save() 中使用 F 表达式
post = Post.objects.get(id=1)
post.views = F('views') + 1
post.save()
# 注意:save() 后 post.views 不会自动更新为新值!
post.refresh_from_db() # 需要重新从数据库读取
print(post.views) # 现在才是正确的值
# ============ 字段间的比较 ============
# 查询 views 大于 likes 的文章(字段与字段比较)
Post.objects.filter(views__gt=F('likes'))
# 查询 created 和 updated 不相同的文章(被更新过的)
Post.objects.filter(updated__gt=F('created'))
# 更复杂的比较
from django.utils import timezone
from datetime import timedelta
# 查询发布超过30天的文章
Post.objects.filter(
publish__lt=timezone.now() - timedelta(days=30)
)
# 使用 F 引用相关对象的字段(通过双下划线)
Post.objects.filter(views__gt=F('category__posts__views'))
# ============ F 表达式的算术运算 ============
from django.db.models import F, ExpressionWrapper, FloatField
# 四则运算
Post.objects.update(views=F('views') * 2) # 浏览量翻倍
Post.objects.update(likes=F('likes') + F('shares')) # 点赞加分享
# 计算字段(用于注解)
posts = Post.objects.annotate(
engagement_rate=ExpressionWrapper(
F('likes') * 100.0 / F('views'), # 互动率 = 点赞/浏览 * 100
output_field=FloatField()
)
).filter(views__gt=0)
# ============ F 表达式用于排序 ============
from django.db.models import F
# 按字段升序,NULL 值排在最后
Post.objects.order_by(F('category').asc(nulls_last=True))
# 按字段降序,NULL 值排在最前
Post.objects.order_by(F('cover_image').desc(nulls_first=True))
2.8.2 Q 对象
Q 对象用于构建复杂的逻辑查询(OR、NOT、AND 的组合)。
from django.db.models import Q
# ============ 基础 Q 对象 ============
# AND 关系(等价于 filter 的多个参数)
posts = Post.objects.filter(Q(status='published') & Q(is_featured=True))
# 等价于:Post.objects.filter(status='published', is_featured=True)
# OR 关系(filter 多个参数做不到!)
posts = Post.objects.filter(Q(status='published') | Q(is_featured=True))
# 查询:已发布 或者 是精选 的文章
# NOT 关系
posts = Post.objects.filter(~Q(status='draft'))
# 等价于:Post.objects.exclude(status='draft')
# ============ 复合 Q 对象 ============
# 复杂条件:(已发布 并且 (是精选 或者 浏览量>1000))
posts = Post.objects.filter(
Q(status='published') & (Q(is_featured=True) | Q(views__gt=1000))
)
# 搜索功能:在标题或正文中查找关键字
def search_posts(keyword):
return Post.objects.filter(
Q(title__icontains=keyword) |
Q(body__icontains=keyword) |
Q(tags__name__icontains=keyword)
).distinct()
# 动态构建 Q 对象
def filter_posts(status=None, author=None, category=None, keyword=None):
"""动态构建查询条件"""
q = Q() # 空 Q 对象(不影响查询)
if status:
q &= Q(status=status)
if author:
q &= Q(author__username=author)
if category:
q &= Q(category__slug=category)
if keyword:
q &= Q(title__icontains=keyword) | Q(body__icontains=keyword)
return Post.objects.filter(q)
# 使用示例
posts = filter_posts(status='published', keyword='Django')
# ============ Q 对象与 filter/exclude 混用 ============
# Q 对象必须在普通参数之前
posts = Post.objects.filter(
Q(status='published') | Q(is_featured=True),
author__username='admin' # 普通参数必须在后面
)
# ============ 实战:高级搜索接口 ============
def advanced_search(request):
"""高级搜索视图"""
keyword = request.GET.get('q', '')
category = request.GET.get('category', '')
start_date = request.GET.get('start_date', '')
end_date = request.GET.get('end_date', '')
order_by = request.GET.get('order', '-publish')
query = Q(status='published')
if keyword:
query &= (
Q(title__icontains=keyword) |
Q(body__icontains=keyword) |
Q(author__username__icontains=keyword)
)
if category:
query &= Q(category__slug=category)
if start_date:
query &= Q(publish__date__gte=start_date)
if end_date:
query &= Q(publish__date__lte=end_date)
posts = Post.objects.filter(query).select_related(
'author', 'category'
).prefetch_related('tags').order_by(order_by)
return posts
2.9 数据库事务(Transaction)
2.9.1 为什么需要事务
事务(Transaction)保证了一组数据库操作要么全部成功,要么全部失败,不会出现部分成功的中间状态。
经典例子:银行转账
A 账户向 B 账户转 100 元:
1. A 账户余额 -100
2. B 账户余额 +100
如果第1步成功,第2步失败:A 少了100元,B 没有收到!
事务保证:如果任一步失败,所有步骤都回滚(恢复原状)
2.9.2 Django 中的事务控制
from django.db import transaction
from blog.models import Post, Comment
# ============ 方式一:atomic() 装饰器 ============
@transaction.atomic
def create_post_with_tags(title, body, author, tag_names):
"""创建文章并关联标签(原子操作)"""
# 如果任何一步失败,整个函数的数据库操作都会回滚
post = Post.objects.create(
title=title,
body=body,
author=author,
slug=slugify(title)
)
for tag_name in tag_names:
tag, _ = Tag.objects.get_or_create(name=tag_name)
post.tags.add(tag)
# 通知相关用户(如果这里抛出异常,文章和标签关联都会回滚)
notify_subscribers(post)
return post
# ============ 方式二:atomic() 上下文管理器 ============
def transfer_credits(from_user, to_user, amount):
"""转移积分"""
with transaction.atomic():
# 扣除积分
from_profile = from_user.profile
if from_profile.credits < amount:
raise ValueError("积分不足")
from_profile.credits = F('credits') - amount
from_profile.save()
# 增加积分
to_profile = to_user.profile
to_profile.credits = F('credits') + amount
to_profile.save()
# 记录日志
CreditLog.objects.create(
from_user=from_user,
to_user=to_user,
amount=amount
)
# with 块之外的代码不在事务中
# ============ 嵌套事务(保存点)============
def complex_operation():
with transaction.atomic():
# 外层事务
post = Post.objects.create(title='Test', ...)
try:
with transaction.atomic():
# 内层事务(使用保存点 SAVEPOINT)
# 如果内层失败,只回滚到保存点,不影响外层
do_risky_operation()
except Exception as e:
# 内层事务失败,外层继续
print(f"可选操作失败:{e}")
# 这里仍然在外层事务中
post.status = 'published'
post.save()
# 外层事务提交,只有 post 创建和状态修改会生效
# ============ 事务的钩子(on_commit)============
# 问题:在事务中发送邮件,如果事务回滚,邮件已经发出去了!
# 解决:使用 on_commit,在事务成功提交后才执行
def send_welcome_email(user):
pass
with transaction.atomic():
user = User.objects.create_user(username='newuser', email='new@example.com')
# ❌ 危险:事务可能回滚,但邮件已发出
# send_welcome_email(user)
# ✅ 安全:事务提交成功后才发邮件
transaction.on_commit(lambda: send_welcome_email(user))
# ============ 手动控制事务 ============
from django.db import connection
# 关闭自动提交(谨慎使用!)
with transaction.atomic():
cursor = connection.cursor()
cursor.execute("SELECT pg_advisory_lock(1)") # 获取数据库锁
# ... 操作 ...
# 事务结束时自动释放锁
# ============ select_for_update(行锁)============
# 防止并发读取同一条记录后同时修改(悲观锁)
with transaction.atomic():
# SELECT ... FOR UPDATE(加行锁,其他事务必须等待)
post = Post.objects.select_for_update().get(id=1)
post.views = post.views + 1
post.save()
# 事务结束时自动释放锁
# nowait=True:不等待,如果锁不可用立即抛出异常
with transaction.atomic():
try:
post = Post.objects.select_for_update(nowait=True).get(id=1)
except transaction.OperationalError:
print("其他事务正在修改此记录,请稍后重试")
2.10 原生 SQL 与 ORM 的混用
有些复杂查询用 ORM 写起来太繁琐,可以直接写 SQL:
from django.db import connection
# ============ 执行原生 SQL ============
# raw():执行 SELECT 查询,返回模型实例(推荐)
posts = Post.objects.raw(
"SELECT * FROM blog_post WHERE status = %s ORDER BY publish DESC",
['published']
)
for post in posts:
print(post.title) # 可以访问模型属性
# 带参数(防 SQL 注入,永远不要用字符串拼接!)
keyword = 'Django'
posts = Post.objects.raw(
"SELECT id, title FROM blog_post WHERE title LIKE %s",
[f'%{keyword}%']
)
# cursor.execute():执行任意 SQL(UPDATE/DELETE/创建表等)
with connection.cursor() as cursor:
cursor.execute("UPDATE blog_post SET views = views + 1 WHERE id = %s", [post_id])
cursor.execute("SELECT COUNT(*) FROM blog_post WHERE status = 'published'")
count = cursor.fetchone()[0]
# 查询多行
with connection.cursor() as cursor:
cursor.execute("SELECT id, title, views FROM blog_post ORDER BY views DESC LIMIT 10")
rows = cursor.fetchall() # 返回元组列表
for row in rows:
print(f"ID: {row[0]}, 标题: {row[1]}, 浏览量: {row[2]}")
# ============ ORM 与原生 SQL 的边界 ============
# 推荐使用 ORM 的情况:
# - 标准的 CRUD 操作
# - 简单到中等复杂度的查询
# - 需要跨数据库兼容
# - 新手开发阶段
# 推荐使用原生 SQL 的情况:
# - 极度复杂的查询(多层嵌套、窗口函数、WITH CTE 等)
# - ORM 生成的 SQL 性能太差
# - 数据库特有的功能(PostgreSQL 的特殊函数等)
# - 批量数据处理、报表统计
# ============ 使用数据库特有函数 ============
# 通过 Func 使用任意 SQL 函数
from django.db.models import Func, CharField, Value
class SHA256(Func):
function = 'SHA256'
arity = 1
output_field = CharField()
# PostgreSQL 的全文搜索
from django.contrib.postgres.search import SearchVector, SearchQuery
Post.objects.annotate(
search=SearchVector('title', 'body')
).filter(search=SearchQuery('Django ORM'))
2.11 ORM 性能优化技巧
2.11.1 常见的 N+1 问题
# ❌ 典型的 N+1 问题
posts = Post.objects.all() # 1 次查询
for post in posts:
print(post.author.username) # N 次查询(每篇文章查一次作者)
print(post.category.name) # N 次查询(每篇文章查一次分类)
for tag in post.tags.all(): # N 次查询(每篇文章查一次标签)
print(tag.name)
# ✅ 使用 select_related 和 prefetch_related 解决 N+1
posts = Post.objects.select_related(
'author', # ForeignKey:用 JOIN 一次查出
'category', # ForeignKey:用 JOIN 一次查出
).prefetch_related(
'tags', # ManyToMany:用额外 IN 查询批量取
).all()
# 总共只需 2-3 次查询,无论有多少篇文章!
for post in posts:
print(post.author.username) # 从缓存读取
print(post.category.name) # 从缓存读取
for tag in post.tags.all(): # 从缓存读取
print(tag.name)
2.11.2 使用 Prefetch 对象精细化控制
from django.db.models import Prefetch
# 对预取的数据进行过滤
posts = Post.objects.prefetch_related(
Prefetch(
'comments',
queryset=Comment.objects.filter(active=True).select_related('author'),
to_attr='active_comments' # 存到自定义属性
)
).all()
for post in posts:
for comment in post.active_comments: # 只有激活的评论
print(comment.body)
# 嵌套预取
posts = Post.objects.prefetch_related(
Prefetch(
'comments',
queryset=Comment.objects.prefetch_related('replies').filter(parent=None)
)
)
2.11.3 只查询需要的字段
# ❌ 查询所有字段(包括 body 这种大字段)
posts = Post.objects.all()
# ✅ 只查询需要的字段(values_list 或 values)
post_list = Post.objects.values_list('id', 'title', 'publish', flat=False)
# ✅ defer():延迟加载指定字段(访问时再查询)
posts = Post.objects.defer('body', 'cover_image')
# body 和 cover_image 暂时不加载,第一次访问时再单独查询
# ✅ only():只加载指定字段(其他字段延迟加载)
posts = Post.objects.only('title', 'publish', 'author_id')
# 注意:defer() 和 only() 返回的仍然是模型实例
# 访问延迟字段时会产生额外查询!
post = Post.objects.defer('body').get(id=1)
print(post.title) # 直接读取
print(post.body) # 这里触发额外查询!
2.11.4 其他性能技巧
# 1. 使用 exists() 代替 count() > 0
# ❌
if Post.objects.filter(slug='my-post').count() > 0:
pass
# ✅
if Post.objects.filter(slug='my-post').exists():
pass
# 2. 索引优化(在 Meta 中定义)
class Post(models.Model):
class Meta:
indexes = [
models.Index(fields=['status', '-publish']), # 复合索引
models.Index(fields=['author', 'status']),
]
# 3. 批量操作代替循环
# ❌ 循环 save(N 次 SQL)
for title in titles:
Post.objects.create(title=title, ...)
# ✅ 批量创建(1 次 SQL)
Post.objects.bulk_create([
Post(title=title, ...) for title in titles
])
# 4. 使用 iterator() 处理大数据集(避免一次性加载全部到内存)
for post in Post.objects.all().iterator(chunk_size=1000):
process(post)
# 5. 使用 explain() 查看查询计划(Django 3.2+)
# 对应 SQL 的 EXPLAIN 语句,帮助你分析查询性能
qs = Post.objects.filter(status='published').order_by('-publish')
print(qs.explain())
print(qs.explain(verbose=True, analyze=True)) # 更详细(PostgreSQL)
2.12 自定义 Manager 与 QuerySet
Manager 是 Django 模型访问数据库的接口(就是 Post.objects),你可以自定义它来封装常用查询。
from django.db import models
from django.utils import timezone
# ============ 方法一:自定义 Manager ============
class PublishedManager(models.Manager):
"""只返回已发布文章的 Manager"""
def get_queryset(self):
# 重写基础 QuerySet,默认只返回已发布的文章
return super().get_queryset().filter(status='published')
def recent(self, days=7):
"""最近 N 天发布的文章"""
cutoff = timezone.now() - timezone.timedelta(days=days)
return self.get_queryset().filter(publish__gte=cutoff)
def featured(self):
"""精选文章"""
return self.get_queryset().filter(is_featured=True)
class FeaturedManager(models.Manager):
"""精选文章的 Manager"""
def get_queryset(self):
return super().get_queryset().filter(
status='published',
is_featured=True
)
# ============ 方法二:自定义 QuerySet(推荐,更灵活)============
class PostQuerySet(models.QuerySet):
"""为 Post 模型定制的 QuerySet,所有方法可链式调用"""
def published(self):
return self.filter(status='published')
def draft(self):
return self.filter(status='draft')
def featured(self):
return self.filter(is_featured=True)
def recent(self, days=7):
cutoff = timezone.now() - timezone.timedelta(days=days)
return self.filter(publish__gte=cutoff)
def by_author(self, username):
return self.filter(author__username=username)
def with_category(self, category_slug):
return self.filter(category__slug=category_slug)
def popular(self, min_views=100):
return self.filter(views__gte=min_views)
def with_all_related(self):
"""预加载所有相关数据(避免 N+1)"""
return self.select_related(
'author', 'category'
).prefetch_related('tags')
def with_comment_count(self):
"""注解评论数量"""
from django.db.models import Count
return self.annotate(comment_count=Count('comments'))
def search(self, keyword):
"""全文搜索"""
from django.db.models import Q
return self.filter(
Q(title__icontains=keyword) |
Q(body__icontains=keyword) |
Q(tags__name__icontains=keyword)
).distinct()
class PostManager(models.Manager):
"""将自定义 QuerySet 绑定到 Manager"""
def get_queryset(self):
return PostQuerySet(self.model, using=self._db)
# 直接代理 QuerySet 方法(可选)
def published(self):
return self.get_queryset().published()
def featured(self):
return self.get_queryset().featured()
class Post(models.Model):
# ... 字段定义 ...
# 默认 Manager(必须放第一位,否则 Django 的很多功能会出问题)
objects = PostManager()
# 额外的 Manager
published = PublishedManager()
featured = FeaturedManager()
# ============ 使用示例 ============
# 使用默认 Manager(PostManager)
all_posts = Post.objects.all()
published_posts = Post.objects.published()
recent_featured = Post.objects.featured().recent(days=3)
# 使用自定义 QuerySet 的链式调用(最灵活!)
posts = Post.objects.get_queryset()\
.published()\
.featured()\
.recent(days=30)\
.by_author('admin')\
.with_all_related()\
.with_comment_count()\
.order_by('-comment_count')
# 使用额外的 Manager
only_published = Post.published.all()
only_featured = Post.featured.recent(days=7)
2.13 Meta 类详解
class Post(models.Model):
# ... 字段 ...
class Meta:
# ============ 基础选项 ============
# 数据库表名(默认:app名_模型名,如 blog_post)
db_table = 'my_custom_post_table'
# 可读名称(单数/复数,用于 Admin 显示)
verbose_name = '文章'
verbose_name_plural = '文章列表'
# 默认排序(影响所有未指定 order_by 的查询)
ordering = ['-publish', 'title']
# ============ 约束选项 ============
# 唯一约束(多字段联合唯一)
unique_together = [['slug', 'publish']]
# Django 4.2+ 推荐使用 constraints
# 使用新式约束(Django 2.2+)
constraints = [
# 唯一约束
models.UniqueConstraint(
fields=['slug', 'author'],
name='unique_slug_per_author'
),
# 条件约束(仅对满足条件的行生效)
models.UniqueConstraint(
fields=['slug'],
condition=models.Q(status='published'),
name='unique_published_slug'
),
# 检查约束(确保字段值满足条件)
models.CheckConstraint(
check=models.Q(views__gte=0),
name='views_non_negative'
),
]
# ============ 索引选项 ============
indexes = [
# 普通索引
models.Index(fields=['status'], name='post_status_idx'),
# 复合索引
models.Index(fields=['-publish', 'status'], name='post_publish_status_idx'),
# 条件索引(仅对满足条件的行建立索引)
models.Index(
fields=['slug'],
condition=models.Q(status='published'),
name='published_post_slug_idx'
),
]
# ============ 权限选项 ============
# 自定义权限(格式:(权限代码, 权限描述))
permissions = [
('can_publish', '可以发布文章'),
('can_feature', '可以设置精选'),
('can_moderate', '可以审核评论'),
]
# 默认权限(覆盖 Django 自动生成的 add/change/delete/view)
default_permissions = ('add', 'change', 'delete', 'view')
# ============ 抽象和代理 ============
# abstract = True:抽象基类,不创建数据库表
# 子类继承该 Meta 的所有选项,并创建自己的表
abstract = False # 默认
# proxy = True:代理模型,不创建新表,但可以有不同的行为
# 常用于给同一个表的数据提供不同的接口
proxy = False # 默认
# ============ 数据库选项 ============
# 指定使用哪个数据库(多数据库时使用)
# app_label = 'blog'
# 指定表的前缀(不常用)
# db_tablespace = 'special_tablespace'
# get_latest_by:默认的 latest()/earliest() 排序字段
get_latest_by = 'publish'
# managed = False:告诉 Django 不管理此表(通常用于已有数据库的反向工程)
# managed = True # 默认
第二部分:Django 中间件深度解析
3.1 中间件是什么
3.1.1 用生活类比理解中间件
想象你去机场坐飞机:
- 进门 → 安检(检查你有没有危险物品)
- 安检通过 → 值机(登记你的信息)
- 值机完成 → 登机(进入飞机/视图)
- 下飞机 → 入境检查(检查护照)
- 检查通过 → 取行李(获取响应内容)
每个环节都是一个"中间件",它们在你到达目的地(视图函数)之前和之后做各种处理。
3.1.2 中间件的核心职责
HTTP 请求 → [中间件1] → [中间件2] → ... → [视图函数] → ... → [中间件2] → [中间件1] → HTTP 响应
中间件可以:
- ✅ 在请求到达视图之前修改请求(添加属性、验证身份等)
- ✅ 在视图处理之前拦截请求(返回错误响应,阻止视图执行)
- ✅ 在响应返回客户端之前修改响应(添加 Header、压缩内容等)
- ✅ 记录日志 、统计耗时 、处理异常
3.2 请求/响应的生命周期
3.2.1 完整的生命周期图
浏览器发起 HTTP 请求
↓
WSGI 服务器
↓
┌─────────────────────────────────────────┐
│ 中间件栈(洋葱模型) │
│ ┌──────────────────────────────────┐ │
│ │ 中间件1.process_request() │ │
│ │ ┌────────────────────────────┐ │ │
│ │ │ 中间件2.process_request() │ │ │
│ │ │ ┌──────────────────────┐ │ │ │
│ │ │ │ 中间件3.process_request│ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ ↓ URL 路由匹配 │ │ │ │
│ │ │ │ 中间件N.process_view│ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ ↓ 视图函数执行 │ │ │ │
│ │ │ │ ↑ 返回 Response │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ 中间件N.process_response│ │ │
│ │ │ └──────────────────────┘ │ │ │
│ │ │ 中间件3.process_response() │ │ │
│ │ └────────────────────────────┘ │ │
│ │ 中间件2.process_response() │ │
│ └──────────────────────────────────┘ │
│ 中间件1.process_response() │
└─────────────────────────────────────────┘
↓
HTTP 响应返回给浏览器
3.2.2 中间件的四个钩子方法
Django 中间件提供了四个可以重写的钩子(Hook)方法:
| 方法 | 调用时机 | 返回值 |
|---|---|---|
process_request(request) |
请求到达,URL 解析之前 | None(继续处理)或 HttpResponse(短路) |
process_view(request, view_func, view_args, view_kwargs) |
URL 解析后,视图执行前 | None 或 HttpResponse |
process_response(request, response) |
视图执行后,响应返回前 | HttpResponse(必须返回!) |
process_exception(request, exception) |
视图抛出异常时 | None 或 HttpResponse |
3.3 Django 内置中间件详解
打开 settings.py,你会看到默认的中间件配置:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
让我们逐一解析每个中间件的作用:
SecurityMiddleware(安全中间件)
# 配置选项(settings.py)
# 1. HTTPS 重定向(生产环境必开!)
SECURE_SSL_REDIRECT = True # 将所有 HTTP 请求重定向到 HTTPS
# 2. HSTS(HTTP 严格传输安全)
SECURE_HSTS_SECONDS = 31536000 # 1年(告诉浏览器以后只用HTTPS)
SECURE_HSTS_INCLUDE_SUBDOMAINS = True # 子域名也强制 HTTPS
SECURE_HSTS_PRELOAD = True # 加入 HSTS 预加载列表
# 3. 防止内容嗅探
SECURE_CONTENT_TYPE_NOSNIFF = True # 添加 X-Content-Type-Options: nosniff 头
# 4. XSS 过滤器(旧浏览器)
SECURE_BROWSER_XSS_FILTER = True # 添加 X-XSS-Protection: 1; mode=block 头
# 5. Referrer 策略
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
# SecurityMiddleware 做的事情:
# - 检查 SECURE_SSL_REDIRECT,如果请求是 HTTP 则重定向到 HTTPS
# - 添加各种安全相关的 HTTP 响应头
# - 检查 Host 头,防止 HTTP Host 头注入攻击
ALLOWED_HOSTS = ['example.com', 'www.example.com'] # 允许的主机名
SessionMiddleware(会话中间件)
# SessionMiddleware 负责管理用户会话(Session)
# 它在 request 对象上附加 session 属性
# 视图中使用 session
def my_view(request):
# 设置 session 值
request.session['user_id'] = 123
request.session['cart'] = {'items': [], 'total': 0}
# 读取 session 值
user_id = request.session.get('user_id', None)
# 删除 session 值
if 'user_id' in request.session:
del request.session['user_id']
# 清空所有 session
request.session.flush()
# 设置 session 过期时间(秒)
request.session.set_expiry(3600) # 1小时后过期
request.session.set_expiry(0) # 关闭浏览器时过期
# Session 相关配置
SESSION_ENGINE = 'django.contrib.sessions.backends.db' # 存储在数据库(默认)
# SESSION_ENGINE = 'django.contrib.sessions.backends.cache' # 存储在缓存(推荐,性能好)
# SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' # 先缓存后数据库
# SESSION_ENGINE = 'django.contrib.sessions.backends.file' # 存储在文件
# SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies' # 存储在 Cookie
SESSION_COOKIE_AGE = 1209600 # Cookie 有效期(2周)
SESSION_COOKIE_SECURE = True # 只通过 HTTPS 传输 Cookie(生产环境)
SESSION_COOKIE_HTTPONLY = True # 禁止 JavaScript 访问 Cookie(防 XSS)
SESSION_SAVE_EVERY_REQUEST = False # 每次请求都保存(默认只在修改时保存)
CsrfViewMiddleware(CSRF 防护中间件)
# CSRF(跨站请求伪造)防护
# 原理:在表单中嵌入随机 Token,服务端验证 Token 是否正确
# 在模板中使用 CSRF Token
"""
<form method="post">
{% csrf_token %} <!-- 自动生成隐藏的 CSRF Token 字段 -->
<input type="text" name="username">
<button type="submit">提交</button>
</form>
"""
# 在 AJAX 请求中使用 CSRF Token
"""
// JavaScript 获取 CSRF Token(从 Cookie 读取)
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
fetch('/api/posts/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken'), // 在请求头中带 CSRF Token
},
body: JSON.stringify({title: 'Test'})
});
"""
# 豁免 CSRF 检查(API 视图常用)
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def api_endpoint(request):
# 该视图不检查 CSRF Token
# 注意:这会降低安全性,通常配合其他认证机制使用
pass
# 设置 CSRF Token 有效期
CSRF_COOKIE_AGE = 31449600 # 约1年(默认)
CSRF_COOKIE_SECURE = True # 只通过 HTTPS 传输(生产环境)
CSRF_COOKIE_HTTPONLY = False # 允许 JavaScript 读取(需要读取 Token)
CSRF_TRUSTED_ORIGINS = ['https://example.com'] # 可信来源
AuthenticationMiddleware(认证中间件)
# AuthenticationMiddleware 负责:
# 1. 从 session 中读取用户信息
# 2. 在 request 对象上附加 user 属性
# 在视图中使用
def my_view(request):
# request.user 是当前用户(匿名用户是 AnonymousUser 实例)
if request.user.is_authenticated:
print(f"已登录用户:{request.user.username}")
else:
print("匿名用户")
# 用户属性
request.user.id # 用户 ID
request.user.username # 用户名
request.user.email # 邮箱
request.user.is_staff # 是否是员工
request.user.is_superuser # 是否是超级管理员
# 权限检查
request.user.has_perm('blog.can_publish') # 单个权限
request.user.has_perms(['blog.add_post', 'blog.change_post']) # 多个权限
request.user.has_module_perms('blog') # 是否有 blog 应用的任何权限
# 登录/登出
from django.contrib.auth import authenticate, login, logout
def login_view(request):
if request.method == 'POST':
username = request.POST['username']
password = request.POST['password']
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user) # 登录(写入 session)
return redirect('home')
else:
return render(request, 'login.html', {'error': '用户名或密码错误'})
def logout_view(request):
logout(request) # 登出(清除 session)
return redirect('home')
3.4 自定义中间件
3.4.1 中间件的两种写法
方式一:函数式中间件(推荐,Django 1.10+)
# myproject/middleware.py
def simple_middleware(get_response):
"""
最简单的函数式中间件模板
外层函数(工厂函数):在服务器启动时执行一次,做初始化操作
内层函数(中间件):每次请求都会执行
"""
# 这里的代码在服务器启动时执行一次(初始化)
print("中间件初始化")
def middleware(request):
# ========== 前置处理(在视图执行前)==========
print(f"请求到来:{request.method} {request.path}")
# 调用下一个中间件(或视图函数)
response = get_response(request)
# ========== 后置处理(在视图执行后)==========
print(f"响应状态码:{response.status_code}")
return response
return middleware
方式二:类式中间件(旧式,但仍然支持)
class SimpleMiddleware:
"""类式中间件"""
def __init__(self, get_response):
self.get_response = get_response
# 初始化代码(服务器启动时执行一次)
def __call__(self, request):
# ========== 前置处理 ==========
# 在视图(和后续中间件)执行之前的代码
response = self.get_response(request)
# ========== 后置处理 ==========
# 在视图(和后续中间件)执行之后的代码
return response
def process_view(self, request, view_func, view_args, view_kwargs):
"""
URL 解析完成后,视图执行前调用
可以拦截并返回自定义响应
返回 None 则继续执行视图
"""
pass
def process_exception(self, request, exception):
"""
视图抛出异常时调用
返回 None 则继续处理异常(传给下一个中间件)
返回 HttpResponse 则停止异常传播
"""
pass
def process_response(self, request, response):
"""
视图返回响应后调用
必须返回 HttpResponse 对象!
"""
return response
3.4.2 注册中间件
写好中间件后,在 settings.py 的 MIDDLEWARE 列表中添加它:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'myproject.middleware.SimpleMiddleware', # 添加你的中间件
# ...
]
3.5 中间件的执行顺序与陷阱
3.5.1 执行顺序
MIDDLEWARE = [
'middleware1', # 第1个(最外层)
'middleware2', # 第2个
'middleware3', # 第3个(最内层)
]
执行顺序:
- 请求处理(从上到下):middleware1 → middleware2 → middleware3 → 视图
- 响应处理(从下到上):视图 → middleware3 → middleware2 → middleware1
这就是著名的洋葱模型:
请求:→ M1 → M2 → M3 → 视图
↑ ↑ ↑
响应:← M1 ← M2 ← M3 ← 视图
3.5.2 顺序的重要性
MIDDLEWARE = [
# 1. SecurityMiddleware 必须第一个
# 因为它负责 HTTPS 重定向,应该最早处理
'django.middleware.security.SecurityMiddleware',
# 2. SessionMiddleware 必须在 AuthenticationMiddleware 之前
# 因为认证中间件依赖 session 来获取用户信息
'django.contrib.sessions.middleware.SessionMiddleware',
# 3. CommonMiddleware 处理 URL 规范化(斜杠等)
'django.middleware.common.CommonMiddleware',
# 4. CsrfViewMiddleware 应该尽早处理
'django.middleware.csrf.CsrfViewMiddleware',
# 5. AuthenticationMiddleware 依赖 SessionMiddleware
# 必须在 SessionMiddleware 之后
'django.contrib.auth.middleware.AuthenticationMiddleware',
# 6. MessageMiddleware 依赖 SessionMiddleware
'django.contrib.messages.middleware.MessageMiddleware',
# 7. XFrameOptionsMiddleware 添加响应头,位置不重要
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# ⚠️ 常见错误:把 AuthenticationMiddleware 放在 SessionMiddleware 之前
# 这会导致 request.user 无法正确获取到用户信息!
3.6 实战案例:各类中间件开发
案例1:请求计时中间件
# middleware/timing.py
import time
import logging
logger = logging.getLogger(__name__)
def timing_middleware(get_response):
"""记录每个请求的处理时间"""
def middleware(request):
start_time = time.time()
response = get_response(request)
duration = time.time() - start_time
duration_ms = duration * 1000 # 转为毫秒
# 添加响应头(方便前端/监控工具查看)
response['X-Request-Duration-Ms'] = str(round(duration_ms, 2))
# 慢请求告警
if duration > 1.0: # 超过1秒
logger.warning(
f"慢请求告警!{request.method} {request.path} "
f"耗时 {duration_ms:.0f}ms,"
f"用户: {getattr(request.user, 'username', '匿名')}"
)
else:
logger.debug(
f"{request.method} {request.path} - {duration_ms:.0f}ms"
)
return response
return middleware
案例2:IP 限流中间件
# middleware/rate_limit.py
from django.http import HttpResponse
from django.core.cache import cache
import time
class RateLimitMiddleware:
"""
基于 IP 地址的简单限流中间件
默认:每分钟最多 100 次请求
"""
RATE_LIMIT = 100 # 允许的最大请求数
TIME_WINDOW = 60 # 时间窗口(秒)
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
ip = self.get_client_ip(request)
if not self.is_allowed(ip):
return HttpResponse(
'请求过于频繁,请稍后再试',
status=429,
content_type='text/plain; charset=utf-8'
)
return self.get_response(request)
def get_client_ip(self, request):
"""获取客户端真实 IP(考虑反向代理)"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0].strip()
return request.META.get('REMOTE_ADDR', '0.0.0.0')
def is_allowed(self, ip):
"""检查该 IP 是否在限流范围内"""
cache_key = f'rate_limit:{ip}'
# 使用 Redis/Memcache 的原子操作
current = cache.get(cache_key, 0)
if current >= self.RATE_LIMIT:
return False
# 原子性递增
try:
cache.incr(cache_key)
except ValueError:
# key 不存在时初始化
cache.set(cache_key, 1, self.TIME_WINDOW)
return True
案例3:用户活跃时间记录中间件
# middleware/user_activity.py
from django.utils import timezone
class UserActivityMiddleware:
"""
记录用户最后活跃时间
每5分钟更新一次(避免频繁数据库写入)
"""
UPDATE_INTERVAL = 300 # 5分钟(秒)
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
# 只记录已登录用户
if request.user.is_authenticated:
self.update_last_seen(request)
return response
def update_last_seen(self, request):
"""更新用户最后活跃时间(带节流)"""
try:
from django.core.cache import cache
cache_key = f'user_last_seen:{request.user.id}'
# 如果5分钟内已经更新过,就不再更新
if not cache.get(cache_key):
# 更新数据库
request.user.profile.last_seen = timezone.now()
request.user.profile.save(update_fields=['last_seen'])
# 设置缓存标记,5分钟内不再更新
cache.set(cache_key, True, self.UPDATE_INTERVAL)
except Exception:
pass # 不因为活跃时间记录失败而影响主流程
案例4:API 版本控制中间件
# middleware/api_version.py
import re
class APIVersionMiddleware:
"""
API 版本控制中间件
支持从 URL 或 Header 中读取版本号
例如:
- URL: /api/v2/posts/
- Header: API-Version: 2
"""
URL_VERSION_PATTERN = re.compile(r'^/api/v(\d+)/')
DEFAULT_VERSION = 1
SUPPORTED_VERSIONS = [1, 2, 3]
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# 从 URL 解析版本号
version = self.get_version_from_url(request.path)
# 如果 URL 没有版本号,从 Header 读取
if version is None:
version = self.get_version_from_header(request)
# 默认版本
if version is None:
version = self.DEFAULT_VERSION
# 检查版本是否支持
if version not in self.SUPPORTED_VERSIONS:
from django.http import JsonResponse
return JsonResponse(
{'error': f'API 版本 v{version} 不支持,支持的版本:{self.SUPPORTED_VERSIONS}'},
status=400
)
# 将版本号附加到 request 对象上(视图中可以用 request.version 访问)
request.version = version
response = self.get_response(request)
# 在响应头中添加版本信息
response['API-Version'] = str(version)
return response
def get_version_from_url(self, path):
match = self.URL_VERSION_PATTERN.match(path)
if match:
return int(match.group(1))
return None
def get_version_from_header(self, request):
version_str = request.META.get('HTTP_API_VERSION')
if version_str:
try:
return int(version_str)
except ValueError:
pass
return None
案例5:请求日志中间件
# middleware/request_log.py
import json
import logging
import time
from django.utils import timezone
logger = logging.getLogger('request_log')
class RequestLogMiddleware:
"""
详细记录所有请求和响应的日志中间件
可用于审计、调试、安全分析
"""
# 不记录这些路径(避免日志过多)
EXCLUDED_PATHS = [
'/health/',
'/favicon.ico',
'/static/',
'/media/',
]
# 敏感字段(记录日志时脱敏)
SENSITIVE_FIELDS = ['password', 'token', 'secret', 'credit_card']
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# 检查是否需要跳过
if self.should_skip(request.path):
return self.get_response(request)
start_time = time.time()
request_time = timezone.now()
# 记录请求体(POST 请求)
request_body = self.get_request_body(request)
response = self.get_response(request)
duration = time.time() - start_time
# 记录日志
log_data = {
'timestamp': request_time.isoformat(),
'method': request.method,
'path': request.path,
'query_string': request.META.get('QUERY_STRING', ''),
'user': getattr(request.user, 'username', None) if hasattr(request, 'user') else None,
'ip': self.get_client_ip(request),
'user_agent': request.META.get('HTTP_USER_AGENT', '')[:200],
'request_body': request_body,
'status_code': response.status_code,
'response_size': len(response.content) if hasattr(response, 'content') else 0,
'duration_ms': round(duration * 1000, 2),
}
if response.status_code >= 500:
logger.error(json.dumps(log_data, ensure_ascii=False))
elif response.status_code >= 400:
logger.warning(json.dumps(log_data, ensure_ascii=False))
else:
logger.info(json.dumps(log_data, ensure_ascii=False))
return response
def should_skip(self, path):
for excluded in self.EXCLUDED_PATHS:
if path.startswith(excluded):
return True
return False
def get_client_ip(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0].strip()
return request.META.get('REMOTE_ADDR', '')
def get_request_body(self, request):
if request.method not in ('POST', 'PUT', 'PATCH'):
return None
try:
content_type = request.content_type or ''
if 'application/json' in content_type:
body = json.loads(request.body)
return self.sanitize_data(body)
elif 'application/x-www-form-urlencoded' in content_type:
return self.sanitize_data(dict(request.POST))
except Exception:
pass
return None
def sanitize_data(self, data):
"""脱敏处理敏感字段"""
if isinstance(data, dict):
sanitized = {}
for key, value in data.items():
if any(sensitive in key.lower() for sensitive in self.SENSITIVE_FIELDS):
sanitized[key] = '***'
else:
sanitized[key] = self.sanitize_data(value)
return sanitized
elif isinstance(data, list):
return [self.sanitize_data(item) for item in data]
return data
案例6:维护模式中间件
# middleware/maintenance.py
from django.conf import settings
from django.http import HttpResponse
from django.template import loader
class MaintenanceModeMiddleware:
"""
维护模式中间件
在 settings.py 中设置 MAINTENANCE_MODE = True 开启维护模式
"""
# 白名单:维护模式下这些路径仍然可以访问
ALLOWED_PATHS = [
'/admin/',
]
# 白名单 IP:维护模式下这些 IP 仍然可以访问
ALLOWED_IPS = getattr(settings, 'MAINTENANCE_ALLOWED_IPS', [])
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if self.is_maintenance_mode():
if not self.is_allowed(request):
return self.maintenance_response(request)
return self.get_response(request)
def is_maintenance_mode(self):
return getattr(settings, 'MAINTENANCE_MODE', False)
def is_allowed(self, request):
# 白名单路径
for path in self.ALLOWED_PATHS:
if request.path.startswith(path):
return True
# 白名单 IP
client_ip = request.META.get('REMOTE_ADDR', '')
if client_ip in self.ALLOWED_IPS:
return True
return False
def maintenance_response(self, request):
try:
template = loader.get_template('maintenance.html')
content = template.render({}, request)
except Exception:
content = '<h1>系统维护中,请稍后再试</h1>'
return HttpResponse(content, status=503)
案例7:多语言/国际化中间件
# middleware/language.py
class AutoLanguageMiddleware:
"""
自动根据用户偏好设置语言的中间件
优先级:URL参数 > Cookie > Accept-Language 头 > 默认语言
"""
LANGUAGE_COOKIE = 'django_language'
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
from django.utils import translation
from django.conf import settings
# 1. 从 URL 参数获取语言
lang = request.GET.get('lang')
# 2. 从 Cookie 获取
if not lang:
lang = request.COOKIES.get(self.LANGUAGE_COOKIE)
# 3. 从 Accept-Language 头获取
if not lang:
accept_language = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
lang = self.parse_accept_language(accept_language)
# 4. 默认语言
if not lang:
lang = settings.LANGUAGE_CODE
# 激活语言
supported_languages = [code for code, name in settings.LANGUAGES]
if lang in supported_languages:
translation.activate(lang)
request.LANGUAGE_CODE = lang
response = self.get_response(request)
# 在响应中设置语言 Cookie
if 'lang' in request.GET:
response.set_cookie(self.LANGUAGE_COOKIE, lang, max_age=365*24*3600)
return response
def parse_accept_language(self, accept_language):
"""解析 Accept-Language 头,返回最优先的语言代码"""
if not accept_language:
return None
languages = []
for item in accept_language.split(','):
parts = item.strip().split(';')
lang = parts[0].strip()
q = 1.0
if len(parts) > 1:
try:
q = float(parts[1].split('=')[1])
except (IndexError, ValueError):
pass
languages.append((lang, q))
languages.sort(key=lambda x: x[1], reverse=True)
return languages[0][0] if languages else None
第三部分:Django 信号深度解析
4.1 信号是什么,为什么需要它
4.1.1 信号的概念
信号(Signal) 是 Django 的一套发布-订阅(Pub/Sub)消息系统 。它允许某些发送者(Sender)在发生特定事件时,通知一组接收者(Receiver) ,而发送者和接收者之间完全解耦,互不知道对方的存在。
4.1.2 为什么需要信号
考虑一个场景:用户注册后需要:
- 创建用户资料(UserProfile)
- 发送欢迎邮件
- 记录注册日志
- 赠送新人积分
不使用信号(耦合的做法):
def register_view(request):
if request.method == 'POST':
form = RegisterForm(request.POST)
if form.is_valid():
user = form.save()
# 以下代码与注册逻辑耦合在一起,违反单一职责原则
UserProfile.objects.create(user=user) # 1. 创建资料
send_welcome_email.delay(user.email) # 2. 发送邮件
logger.info(f"新用户注册:{user.username}") # 3. 记录日志
give_new_user_credits(user, amount=100) # 4. 赠送积分
return redirect('home')
问题:
- 注册视图承担了太多职责
- 新增功能(如第5步)需要修改注册视图代码
- 测试困难,各逻辑混杂
使用信号(解耦的做法):
# 注册视图只关心注册本身
def register_view(request):
if request.method == 'POST':
form = RegisterForm(request.POST)
if form.is_valid():
user = form.save() # 保存用户,Django 自动发送 post_save 信号
return redirect('home')
# 各模块各自监听信号,互不干扰
# 在 blog/signals.py 中:
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
@receiver(post_save, sender=User)
def on_user_created(sender, instance, created, **kwargs):
if created: # 只在新建用户时处理
UserProfile.objects.create(user=instance) # 1. 创建资料
@receiver(post_save, sender=User)
def send_welcome_email_on_register(sender, instance, created, **kwargs):
if created:
send_welcome_email.delay(instance.email) # 2. 发送邮件
# ... 可以无限扩展,不影响注册视图
信号的好处:
- ✅ 解耦:发送者和接收者互不依赖
- ✅ 扩展性强:添加新功能只需添加新的接收者
- ✅ 单一职责:每个处理函数只做一件事
- ✅ 可复用:信号可以被多个接收者监听
4.2 Django 内置信号大全
4.2.1 模型信号
from django.db.models.signals import (
pre_init, # 模型 __init__() 方法调用前
post_init, # 模型 __init__() 方法调用后
pre_save, # save() 方法调用前
post_save, # save() 方法调用后 ← 最常用
pre_delete, # delete() 方法调用前
post_delete, # delete() 方法调用后
m2m_changed, # 多对多关系改变时(add/remove/clear/set)
pre_migrate, # 执行 migrate 命令前
post_migrate, # 执行 migrate 命令后
class_prepared, # 模型类定义完成时
)
pre_save 和 post_save 的参数:
from django.db.models.signals import pre_save, post_save
from django.dispatch import receiver
from blog.models import Post
@receiver(pre_save, sender=Post)
def before_post_save(sender, instance, **kwargs):
"""
sender : 发送信号的模型类(Post)
instance : 即将保存的 Post 实例
kwargs : 其他关键字参数(包括 raw、using 等)
注意:pre_save 时 instance 尚未写入数据库!
常用场景:在保存前修改数据、验证数据
"""
# 自动生成 slug
if not instance.slug:
from django.utils.text import slugify
instance.slug = slugify(instance.title)
# 自动生成摘要
if not instance.summary and instance.body:
instance.summary = instance.body[:200]
@receiver(post_save, sender=Post)
def after_post_save(sender, instance, created, **kwargs):
"""
sender : 发送信号的模型类(Post)
instance : 刚保存的 Post 实例(已有 id)
created : 布尔值,True = 新建,False = 更新 ← 非常重要!
raw : 布尔值,True = 通过 fixtures 加载数据
using : 数据库别名(多数据库时使用)
update_fields : 调用 save(update_fields=[...]) 时,更新的字段名集合
"""
if created:
# 新建时的操作
print(f"文章 '{instance.title}' 已创建")
else:
# 更新时的操作
print(f"文章 '{instance.title}' 已更新")
@receiver(pre_delete, sender=Post)
def before_post_delete(sender, instance, **kwargs):
"""
在文章删除前调用
常用场景:清理相关文件、发送通知
"""
# 删除封面图片文件
if instance.cover_image:
import os
if os.path.isfile(instance.cover_image.path):
os.remove(instance.cover_image.path)
print(f"已删除封面图片:{instance.cover_image.path}")
@receiver(post_delete, sender=Post)
def after_post_delete(sender, instance, **kwargs):
"""
在文章删除后调用
注意:此时 instance.pk 已经是 None(记录已删除)
"""
print(f"文章 '{instance.title}' 已删除")
m2m_changed 信号详解:
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from blog.models import Post
@receiver(m2m_changed, sender=Post.tags.through)
def on_post_tags_changed(sender, instance, action, reverse, model, pk_set, **kwargs):
"""
sender : 中间表模型(Post_tags 这样的自动创建的表)
instance : 发生变化的对象(Post 或 Tag,取决于操作方向)
action : 操作类型(见下方)
reverse : 是否从 Tag 一侧操作(反向操作)
model : 被添加/删除的模型类
pk_set : 被添加/删除的对象的主键集合
action 的可能值:
'pre_add' : 调用 add() 前
'post_add' : 调用 add() 后
'pre_remove' : 调用 remove() 前
'post_remove': 调用 remove() 后
'pre_clear' : 调用 clear() 前
'post_clear' : 调用 clear() 后
"""
if action == 'post_add':
tag_count = instance.tags.count()
print(f"文章 '{instance.title}' 添加了 {len(pk_set)} 个标签,现有 {tag_count} 个标签")
elif action == 'post_remove':
print(f"文章 '{instance.title}' 移除了 {len(pk_set)} 个标签")
elif action == 'post_clear':
print(f"文章 '{instance.title}' 清空了所有标签")
4.2.2 请求/响应信号
from django.core.signals import (
request_started, # HTTP 请求开始时(在任何中间件之前)
request_finished, # HTTP 请求完成时(响应发送后)
got_request_exception, # 请求处理过程中发生异常时
)
from django.dispatch import receiver
@receiver(request_started)
def on_request_started(sender, environ, **kwargs):
"""
每个 HTTP 请求开始时触发
environ: WSGI 环境变量字典
"""
pass
@receiver(request_finished)
def on_request_finished(sender, **kwargs):
"""
每个 HTTP 请求完成时触发
"""
pass
@receiver(got_request_exception)
def on_request_exception(sender, request, **kwargs):
"""
请求处理出现未捕获异常时触发
可用于异常监控、告警
"""
import traceback
print(f"请求 {request.path} 发生异常:{traceback.format_exc()}")
4.2.3 认证相关信号
from django.contrib.auth.signals import (
user_logged_in, # 用户登录成功
user_logged_out, # 用户登出
user_login_failed, # 用户登录失败
)
from django.dispatch import receiver
@receiver(user_logged_in)
def on_user_login(sender, request, user, **kwargs):
"""
用户成功登录时触发
"""
from blog.models import LoginLog
LoginLog.objects.create(
user=user,
ip=request.META.get('REMOTE_ADDR', ''),
user_agent=request.META.get('HTTP_USER_AGENT', '')[:200],
status='success'
)
print(f"用户 {user.username} 登录成功,IP: {request.META.get('REMOTE_ADDR')}")
@receiver(user_logged_out)
def on_user_logout(sender, request, user, **kwargs):
"""
用户登出时触发
user 可能为 None(如果用户不存在)
"""
if user:
print(f"用户 {user.username} 已登出")
@receiver(user_login_failed)
def on_login_failed(sender, credentials, request, **kwargs):
"""
用户登录失败时触发
credentials: 提交的登录凭证(dict),注意:包含密码,不要记录原文!
"""
from blog.models import LoginLog
username = credentials.get('username', '未知')
ip = request.META.get('REMOTE_ADDR', '')
LoginLog.objects.create(
username=username,
ip=ip,
status='failed'
)
# 检测是否暴力破解(1分钟内失败超过5次则封锁IP)
from django.core.cache import cache
from django.utils import timezone
key = f'login_failed:{ip}'
fail_count = cache.get(key, 0) + 1
cache.set(key, fail_count, 60) # 1分钟计数窗口
if fail_count >= 5:
print(f"⚠️ 检测到暴力破解!IP {ip} 1分钟内登录失败 {fail_count} 次")
# 可以封锁 IP、发送告警邮件等
4.2.4 数据库相关信号
from django.db.models.signals import pre_migrate, post_migrate
from django.dispatch import receiver
@receiver(post_migrate)
def on_post_migrate(sender, app_config, verbosity, interactive, using, **kwargs):
"""
migrate 命令执行完成后触发
常用于:创建初始数据、创建默认分组/权限等
sender : AppConfig 实例
app_config : 刚刚完成迁移的应用配置
verbosity : 输出详细程度(0=静默, 1=普通, 2=详细, 3=非常详细)
"""
if app_config.name == 'blog':
# 创建默认分类
from blog.models import Category
Category.objects.get_or_create(
slug='uncategorized',
defaults={'name': '未分类', 'description': '默认分类'}
)
# 创建默认权限组
from django.contrib.auth.models import Group, Permission
editor_group, created = Group.objects.get_or_create(name='编辑')
if created:
permissions = Permission.objects.filter(
content_type__app_label='blog',
codename__in=['add_post', 'change_post', 'view_post']
)
editor_group.permissions.set(permissions)
print("已创建编辑权限组")
4.3 信号的连接方式
4.3.1 使用 @receiver 装饰器(推荐)
from django.db.models.signals import post_save
from django.dispatch import receiver
from blog.models import Post
# 基础用法
@receiver(post_save, sender=Post)
def my_handler(sender, instance, created, **kwargs):
pass
# 同一个函数监听多个信号
@receiver([post_save, post_delete], sender=Post)
def handle_post_change(sender, instance, **kwargs):
"""文章新建、修改、删除时都会触发"""
pass
# 监听多个模型
from blog.models import Post, Comment
@receiver(post_save, sender=Post)
@receiver(post_save, sender=Comment)
def handle_content_save(sender, instance, created, **kwargs):
"""文章或评论保存时触发"""
pass
4.3.2 使用 connect() 方法手动连接
from django.db.models.signals import post_save
from blog.models import Post
def my_handler(sender, instance, created, **kwargs):
pass
# 连接信号
post_save.connect(my_handler, sender=Post)
# 断开信号
post_save.disconnect(my_handler, sender=Post)
# 弱引用(weak=False 确保接收器不会被垃圾回收)
post_save.connect(my_handler, sender=Post, weak=False)
# dispatch_uid:唯一标识,防止重复连接同一个处理函数
post_save.connect(my_handler, sender=Post, dispatch_uid='blog.post_save_handler')
4.3.3 在 AppConfig 中注册信号(最佳实践)
第一步:创建 signals.py 文件
# blog/signals.py
from django.db.models.signals import post_save, post_delete, m2m_changed
from django.contrib.auth.signals import user_logged_in
from django.dispatch import receiver
from django.contrib.auth.models import User
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
"""用户创建时自动创建 UserProfile"""
if created:
from blog.models import UserProfile
UserProfile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
"""用户保存时同步保存 UserProfile"""
if hasattr(instance, 'profile'):
instance.profile.save()
# 更多信号处理函数...
第二步:在 AppConfig 中导入 signals
# blog/apps.py
from django.apps import AppConfig
class BlogConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'blog'
verbose_name = '博客'
def ready(self):
"""
当 Django 启动并且应用配置完成后调用
这是注册信号处理函数的正确位置!
"""
import blog.signals # 导入 signals 模块,完成信号注册
第三步:确保 apps.py 中的 AppConfig 被使用
# blog/__init__.py
default_app_config = 'blog.apps.BlogConfig'
⚠️ 为什么要在 ready() 中导入?
如果在模块顶层导入信号处理函数,可能会在 Django 尚未完全初始化时就尝试访问数据库或模型,导致
AppRegistryNotReady错误。ready()方法在所有应用加载完毕后才调用,是最安全的信号注册时机。
4.4 自定义信号
当内置信号无法满足需求时,你可以创建自己的信号:
# blog/signals.py
from django.dispatch import Signal
# ============ 定义自定义信号 ============
# 文章发布信号(当文章状态变为 published 时)
post_published = Signal()
# 可以在文档中说明会传递哪些参数:
# 发送时:post_published.send(sender=Post, instance=post, publisher=user)
# 接收时:def handler(sender, instance, publisher, **kwargs)
# 文章阅读信号
post_viewed = Signal()
# 传递:post_viewed.send(sender=Post, instance=post, viewer=user, ip=ip_address)
# 评论点赞信号
comment_liked = Signal()
# 传递:comment_liked.send(sender=Comment, instance=comment, liker=user)
# 用户积分变动信号
credits_changed = Signal()
# 传递:credits_changed.send(sender=UserProfile, instance=profile,
# amount=amount, reason='post_published')
# ============ 发送自定义信号 ============
# 在视图或模型方法中发送信号:
class Post(models.Model):
# ... 字段 ...
def publish(self, publisher):
"""发布文章"""
if self.status != 'published':
self.status = 'published'
self.publish = timezone.now()
self.save()
# 发送自定义信号
post_published.send(
sender=self.__class__,
instance=self,
publisher=publisher
)
def record_view(self, viewer=None, ip=None):
"""记录阅读"""
self.views = F('views') + 1
self.save(update_fields=['views'])
# 发送阅读信号
post_viewed.send(
sender=self.__class__,
instance=self,
viewer=viewer,
ip=ip
)
# ============ 接收自定义信号 ============
from django.dispatch import receiver
@receiver(post_published)
def on_post_published(sender, instance, publisher, **kwargs):
"""文章发布后:发送通知、赠送积分等"""
# 1. 通知订阅者
from blog.tasks import notify_subscribers_async
notify_subscribers_async.delay(instance.id)
# 2. 给作者赠送积分
profile = publisher.profile
credits_changed.send(
sender=profile.__class__,
instance=profile,
amount=50,
reason=f'发布文章《{instance.title}》'
)
# 3. 推送到社交媒体
if instance.is_featured:
push_to_social_media.delay(instance.id)
@receiver(post_viewed)
def on_post_viewed(sender, instance, viewer, ip, **kwargs):
"""文章被阅读后:记录阅读历史"""
if viewer and viewer.is_authenticated:
# 记录已读(避免重复推荐)
ReadHistory.objects.get_or_create(
user=viewer,
post=instance
)
@receiver(credits_changed)
def on_credits_changed(sender, instance, amount, reason, **kwargs):
"""积分变动后:记录日志、发送通知"""
# 记录积分日志
CreditLog.objects.create(
user=instance.user,
amount=amount,
reason=reason,
balance_after=instance.credits
)
# 积分里程碑通知
milestone_credits = [100, 500, 1000, 5000, 10000]
for milestone in milestone_credits:
if instance.credits >= milestone and (instance.credits - amount) < milestone:
send_milestone_notification.delay(instance.user.id, milestone)
break
4.5 信号的实战案例
案例1:自动创建用户资料(最经典的信号用法)
# blog/models.py
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
bio = models.TextField(blank=True, verbose_name='个人简介')
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
website = models.URLField(blank=True)
credits = models.PositiveIntegerField(default=0, verbose_name='积分')
last_seen = models.DateTimeField(null=True, blank=True)
def __str__(self):
return f'{self.user.username} 的资料'
# blog/signals.py
@receiver(post_save, sender=User)
def create_or_update_user_profile(sender, instance, created, **kwargs):
"""
用户创建或更新时,自动管理 UserProfile
"""
if created:
# 新建用户时,创建对应的 UserProfile
UserProfile.objects.create(user=instance)
else:
# 更新用户时,确保 UserProfile 也被保存
# (处理 UserProfile 可能不存在的边缘情况)
UserProfile.objects.get_or_create(user=instance)
# 注意:如果你修改了 Profile 字段并保存 user,不会自动更新 Profile
# 需要明确调用 user.profile.save()
案例2:文章发布时自动生成 SEO 信息
# blog/signals.py
from django.utils.text import slugify
@receiver(pre_save, sender=Post)
def auto_generate_post_fields(sender, instance, **kwargs):
"""
文章保存前,自动生成各种字段
"""
# 1. 自动生成 slug(仅在新建且未设置时)
if not instance.pk and not instance.slug:
base_slug = slugify(instance.title, allow_unicode=True)
slug = base_slug
counter = 1
# 确保 slug 唯一
while Post.objects.filter(slug=slug).exists():
slug = f"{base_slug}-{counter}"
counter += 1
instance.slug = slug
# 2. 自动生成摘要(如果未手动填写)
if not instance.summary and instance.body:
# 去除 HTML 标签,取前200字
import re
clean_body = re.sub(r'<[^>]+>', '', instance.body)
instance.summary = clean_body[:200].strip()
if len(clean_body) > 200:
instance.summary += '...'
# 3. 记录修改前的状态(用于后续处理)
if instance.pk:
try:
old_instance = Post.objects.get(pk=instance.pk)
instance._previous_status = old_instance.status
except Post.DoesNotExist:
instance._previous_status = None
@receiver(post_save, sender=Post)
def handle_post_status_change(sender, instance, created, **kwargs):
"""
处理文章状态变化
"""
if created:
return # 新建时不处理
previous_status = getattr(instance, '_previous_status', None)
current_status = instance.status
if previous_status == 'draft' and current_status == 'published':
# 从草稿变为发布:发送通知
print(f"🎉 文章《{instance.title}》已发布!")
# 给作者赠送积分
try:
profile = instance.author.profile
profile.credits = F('credits') + 50
profile.save(update_fields=['credits'])
except Exception:
pass
elif previous_status == 'published' and current_status == 'draft':
# 从发布变为草稿:撤回通知
print(f"文章《{instance.title}》已撤回")
案例3:评论系统的信号处理
# blog/signals.py
@receiver(post_save, sender=Comment)
def on_comment_created(sender, instance, created, **kwargs):
"""
评论创建后的处理
"""
if not created:
return
if not instance.active:
return
# 1. 通知文章作者(如果评论者不是作者本人)
if instance.author != instance.post.author:
# 创建站内通知
Notification.objects.create(
recipient=instance.post.author,
sender=instance.author,
notification_type='comment',
post=instance.post,
comment=instance,
message=f'{instance.author.username} 评论了你的文章《{instance.post.title}》'
)
# 2. 如果是回复某条评论,通知被回复者
if instance.parent and instance.author != instance.parent.author:
Notification.objects.create(
recipient=instance.parent.author,
sender=instance.author,
notification_type='reply',
post=instance.post,
comment=instance,
message=f'{instance.author.username} 回复了你的评论'
)
# 3. 更新文章的评论计数(可选,也可以每次计算)
# 注意:此时评论已经保存,不要造成递归调用
Post.objects.filter(pk=instance.post.pk).update(
comment_count=F('comment_count') + 1
)
@receiver(post_delete, sender=Comment)
def on_comment_deleted(sender, instance, **kwargs):
"""
评论删除后的处理
"""
# 更新评论计数
Post.objects.filter(pk=instance.post.pk).update(
comment_count=models.greatest(F('comment_count') - 1, 0)
)
# 删除相关通知
Notification.objects.filter(comment=instance).delete()
案例4:文件清理信号
# blog/signals.py
import os
from django.db.models.signals import pre_save, post_delete
from django.dispatch import receiver
from blog.models import Post, UserProfile
def delete_file(file_field):
"""删除文件字段对应的文件"""
if file_field and hasattr(file_field, 'path'):
try:
if os.path.isfile(file_field.path):
os.remove(file_field.path)
print(f"已删除文件:{file_field.path}")
except Exception as e:
print(f"删除文件失败:{e}")
@receiver(pre_save, sender=Post)
def delete_old_cover_on_update(sender, instance, **kwargs):
"""
文章更新时,如果封面图片发生变化,删除旧图片
"""
if not instance.pk:
return # 新建时不处理
try:
old_instance = Post.objects.get(pk=instance.pk)
except Post.DoesNotExist:
return
old_cover = old_instance.cover_image
new_cover = instance.cover_image
# 如果封面图片发生了变化,删除旧图片
if old_cover and old_cover != new_cover:
delete_file(old_cover)
@receiver(post_delete, sender=Post)
def delete_cover_on_post_delete(sender, instance, **kwargs):
"""
文章删除时,删除封面图片
"""
delete_file(instance.cover_image)
@receiver(pre_save, sender=UserProfile)
def delete_old_avatar_on_update(sender, instance, **kwargs):
"""
用户资料更新时,如果头像发生变化,删除旧头像
"""
if not instance.pk:
return
try:
old_instance = UserProfile.objects.get(pk=instance.pk)
except UserProfile.DoesNotExist:
return
if old_instance.avatar and old_instance.avatar != instance.avatar:
delete_file(old_instance.avatar)
案例5:缓存失效信号
# blog/signals.py
from django.core.cache import cache
from django.db.models.signals import post_save, post_delete, m2m_changed
from django.dispatch import receiver
from blog.models import Post, Category, Tag
def invalidate_post_cache(post_id=None, author_id=None, category_id=None):
"""
使相关缓存失效
"""
keys_to_delete = []
# 首页缓存
keys_to_delete.append('homepage_posts')
keys_to_delete.append('featured_posts')
# 文章详情缓存
if post_id:
keys_to_delete.append(f'post_detail_{post_id}')
keys_to_delete.append(f'post_comments_{post_id}')
# 作者文章列表缓存
if author_id:
keys_to_delete.append(f'author_posts_{author_id}')
# 分类文章列表缓存
if category_id:
keys_to_delete.append(f'category_posts_{category_id}')
# 批量删除缓存
cache.delete_many(keys_to_delete)
@receiver(post_save, sender=Post)
def invalidate_cache_on_post_save(sender, instance, **kwargs):
"""文章保存时,使相关缓存失效"""
invalidate_post_cache(
post_id=instance.pk,
author_id=instance.author_id,
category_id=instance.category_id
)
@receiver(post_delete, sender=Post)
def invalidate_cache_on_post_delete(sender, instance, **kwargs):
"""文章删除时,使相关缓存失效"""
invalidate_post_cache(
post_id=instance.pk,
author_id=instance.author_id,
category_id=instance.category_id
)
@receiver(m2m_changed, sender=Post.tags.through)
def invalidate_cache_on_tags_changed(sender, instance, action, **kwargs):
"""标签关系变化时,使相关缓存失效"""
if action in ('post_add', 'post_remove', 'post_clear'):
invalidate_post_cache(post_id=instance.pk)
4.6 信号的常见坑与最佳实践
4.6.1 常见问题
问题1:信号被多次触发
# ❌ 错误:每次导入 signals.py 都会重复注册,导致信号被多次触发
# blog/views.py
from blog import signals # 每次 import 都注册一次!
# ✅ 正确:只在 AppConfig.ready() 中导入一次
# blog/apps.py
class BlogConfig(AppConfig):
def ready(self):
import blog.signals # 只执行一次
# 或者使用 dispatch_uid 避免重复注册
post_save.connect(
my_handler,
sender=Post,
dispatch_uid='my_unique_handler_id' # 唯一ID,防止重复注册
)
问题2:信号处理中的数据库查询导致 N+1
# ❌ 低效:每次保存文章都查询作者资料
@receiver(post_save, sender=Post)
def handler(sender, instance, **kwargs):
profile = UserProfile.objects.get(user=instance.author) # 额外查询
profile.post_count += 1
profile.save()
# ✅ 高效:使用 select_related 或直接更新
@receiver(post_save, sender=Post)
def handler(sender, instance, created, **kwargs):
if created:
UserProfile.objects.filter(
user_id=instance.author_id # 使用 _id 字段,无需查 Post
).update(post_count=F('post_count') + 1)
问题3:信号中修改对象导致无限递归
# ❌ 危险:在 post_save 信号中调用 save(),导致无限递归!
@receiver(post_save, sender=Post)
def handler(sender, instance, **kwargs):
instance.slug = slugify(instance.title)
instance.save() # 这会再次触发 post_save 信号,无限递归!
# ✅ 解决方案1:使用 pre_save 信号(在保存前修改,不需要再 save)
@receiver(pre_save, sender=Post)
def handler(sender, instance, **kwargs):
instance.slug = slugify(instance.title)
# 不调用 save(),由 Django 自动保存修改
# ✅ 解决方案2:在 post_save 中使用 update() 而非 save()
@receiver(post_save, sender=Post)
def handler(sender, instance, **kwargs):
Post.objects.filter(pk=instance.pk).update(
slug=slugify(instance.title)
) # update() 不会触发 post_save 信号!
# ✅ 解决方案3:使用标志位防止递归
@receiver(post_save, sender=Post)
def handler(sender, instance, **kwargs):
if hasattr(instance, '_signal_handling'):
return # 防止递归
instance._signal_handling = True
instance.slug = slugify(instance.title)
instance.save(update_fields=['slug'])
del instance._signal_handling
问题4:信号中的异常处理
# ❌ 危险:信号处理中的异常会冒泡到视图,导致请求失败!
@receiver(post_save, sender=Post)
def send_notification(sender, instance, created, **kwargs):
if created:
send_email(instance.author.email, ...) # 如果邮件发送失败,文章保存也会失败!
# ✅ 正确:在信号处理中捕获异常
import logging
logger = logging.getLogger(__name__)
@receiver(post_save, sender=Post)
def send_notification(sender, instance, created, **kwargs):
if not created:
return
try:
send_email(instance.author.email, ...)
except Exception as e:
logger.error(f"发送通知邮件失败:{e}", exc_info=True)
# 不向上抛出,让主流程继续
问题5:信号与事务的关系
# ❌ 问题:信号在事务提交之前触发,如果事务回滚,信号操作不会回滚!
@transaction.atomic
def create_post(data):
post = Post.objects.create(**data)
# post_save 信号在这里触发,但事务还没提交!
# 如果后续代码抛出异常,post 被回滚,但信号操作(如发邮件)已经执行了!
raise SomeException() # 事务回滚,但邮件已发出!
# ✅ 解决方案:使用 transaction.on_commit
from django.db import transaction
@receiver(post_save, sender=Post)
def send_notification(sender, instance, created, **kwargs):
if not created:
return
# 在事务成功提交后才执行
transaction.on_commit(
lambda: send_email_task.delay(instance.pk)
)
4.6.2 最佳实践总结
# best_practices.py
"""
Django 信号最佳实践总结:
1. 📍 始终在 AppConfig.ready() 中注册信号
- 避免 AppRegistryNotReady 错误
- 确保只注册一次
2. 🔑 使用 dispatch_uid 防止重复注册
post_save.connect(handler, sender=Post, dispatch_uid='unique_id')
3. 🔒 在信号处理函数中捕获所有异常
- 防止信号处理的失败影响主业务流程
4. 💾 使用事务钩子处理依赖事务提交的操作
transaction.on_commit(lambda: task.delay(instance.pk))
5. ⚡ 避免在信号中执行耗时操作
- 发邮件、推送通知等应该放到 Celery 异步任务中
6. 🔄 避免在 post_save 中调用 save()(无限递归风险)
- 改用 pre_save 修改数据
- 或使用 update() 方法(不触发信号)
7. 📊 使用 F() 表达式做原子性更新
- 避免读改写中的并发问题
8. 🧪 为信号处理函数编写单元测试
- 测试各种触发条件和边界情况
9. 📝 在 created 参数上做好区分
- created=True:新建,才执行某些操作(如发欢迎邮件)
- created=False:更新,执行另一些操作
10. 🏷️ 不要过度使用信号
- 如果逻辑复杂,直接在视图或模型方法中调用更清晰
- 信号适合于:自动化、解耦、跨模块通信
"""
# 完整的信号处理函数模板
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.db import transaction
import logging
logger = logging.getLogger(__name__)
@receiver(post_save, sender=Post, dispatch_uid='blog.post_post_save_handler')
def post_save_handler(sender, instance, created, raw, **kwargs):
"""
文章保存信号处理函数(完整的最佳实践版本)
参数:
sender: 模型类
instance: 实例
created: 是否新建
raw: 是否通过 loaddata/fixture 加载(True时通常应该跳过处理)
"""
# 跳过 fixture 加载时的信号(避免数据导入时执行额外操作)
if raw:
return
try:
if created:
# 新建文章的处理
# 使用 on_commit 确保数据库操作成功后再执行
transaction.on_commit(
lambda: handle_new_post.delay(instance.pk) # Celery 异步任务
)
else:
# 更新文章的处理
pass
except Exception as e:
logger.error(
f"post_save_handler 处理失败:post_id={instance.pk}, error={e}",
exc_info=True
)
第四部分:综合实战项目
5.1 博客系统完整实现
将前三部分的知识综合起来,实现一个完整的博客系统:
5.1.1 完整的模型设计
# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.urls import reverse
from django.utils.text import slugify
import uuid
class TimeStampedModel(models.Model):
"""
抽象基类:所有模型都继承它,自动获得创建时间和更新时间字段
abstract = True 意味着不会创建数据库表
"""
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
abstract = True
class Category(TimeStampedModel):
"""文章分类"""
name = models.CharField(max_length=100, unique=True, verbose_name='名称')
slug = models.SlugField(max_length=100, unique=True, verbose_name='URL别名')
description = models.TextField(blank=True, verbose_name='描述')
parent = models.ForeignKey(
'self', on_delete=models.SET_NULL,
null=True, blank=True,
related_name='children',
verbose_name='父分类'
)
sort_order = models.PositiveSmallIntegerField(default=0, verbose_name='排序')
class Meta:
verbose_name = '分类'
verbose_name_plural = '分类'
ordering = ['sort_order', 'name']
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name, allow_unicode=True)
super().save(*args, **kwargs)
class Tag(TimeStampedModel):
"""文章标签"""
name = models.CharField(max_length=50, unique=True, verbose_name='标签名')
slug = models.SlugField(max_length=50, unique=True, verbose_name='URL别名')
color = models.CharField(max_length=7, default='#6c757d', verbose_name='颜色')
class Meta:
verbose_name = '标签'
verbose_name_plural = '标签'
ordering = ['name']
def __str__(self):
return self.name
class PostQuerySet(models.QuerySet):
"""自定义 Post QuerySet"""
def published(self):
return self.filter(status='published', publish__lte=timezone.now())
def draft(self):
return self.filter(status='draft')
def featured(self):
return self.published().filter(is_featured=True)
def by_author(self, user):
return self.filter(author=user)
def by_category(self, category):
return self.published().filter(category=category)
def by_tag(self, tag):
return self.published().filter(tags=tag)
def search(self, query):
from django.db.models import Q
return self.published().filter(
Q(title__icontains=query) |
Q(body__icontains=query) |
Q(summary__icontains=query) |
Q(tags__name__icontains=query)
).distinct()
def with_counts(self):
from django.db.models import Count
return self.annotate(
comment_count=Count('comments', filter=models.Q(comments__active=True))
)
def popular(self):
return self.published().order_by('-views', '-publish')
def recent(self):
return self.published().order_by('-publish')
class PostManager(models.Manager):
def get_queryset(self):
return PostQuerySet(self.model, using=self._db).select_related('author', 'category')
def published(self):
return self.get_queryset().published()
def featured(self):
return self.get_queryset().featured()
def search(self, query):
return self.get_queryset().search(query)
class Post(TimeStampedModel):
"""博客文章"""
STATUS_DRAFT = 'draft'
STATUS_PUBLISHED = 'published'
STATUS_CHOICES = [
(STATUS_DRAFT, '草稿'),
(STATUS_PUBLISHED, '已发布'),
]
uid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
title = models.CharField(max_length=300, verbose_name='标题')
slug = models.SlugField(max_length=300, allow_unicode=True, verbose_name='URL别名')
author = models.ForeignKey(
User, on_delete=models.CASCADE,
related_name='posts', verbose_name='作者'
)
category = models.ForeignKey(
Category, on_delete=models.SET_NULL,
null=True, blank=True,
related_name='posts', verbose_name='分类'
)
tags = models.ManyToManyField(
Tag, blank=True, related_name='posts', verbose_name='标签'
)
body = models.TextField(verbose_name='正文')
summary = models.CharField(max_length=500, blank=True, verbose_name='摘要')
cover_image = models.ImageField(
upload_to='posts/%Y/%m/', blank=True, null=True, verbose_name='封面图'
)
status = models.CharField(
max_length=10, choices=STATUS_CHOICES,
default=STATUS_DRAFT, verbose_name='状态'
)
publish = models.DateTimeField(default=timezone.now, verbose_name='发布时间')
views = models.PositiveIntegerField(default=0, verbose_name='浏览量')
is_featured = models.BooleanField(default=False, verbose_name='精选')
allow_comments = models.BooleanField(default=True, verbose_name='允许评论')
# 使用自定义 Manager
objects = PostManager()
class Meta:
verbose_name = '文章'
verbose_name_plural = '文章'
ordering = ['-publish']
indexes = [
models.Index(fields=['-publish']),
models.Index(fields=['status', '-publish']),
models.Index(fields=['author', 'status']),
]
constraints = [
models.UniqueConstraint(
fields=['slug', 'author'],
name='unique_slug_per_author'
)
]
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('blog:post_detail', kwargs={'pk': self.pk})
def is_published(self):
return self.status == self.STATUS_PUBLISHED
def increment_views(self):
"""安全地增加浏览量"""
Post.objects.filter(pk=self.pk).update(views=models.F('views') + 1)
5.1.2 视图层整合 ORM、中间件、信号
# blog/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, Http404
from django.core.paginator import Paginator
from django.contrib import messages
from django.db import transaction
from django.views.decorators.http import require_POST
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
from django.views.generic import ListView, DetailView, CreateView, UpdateView
from .models import Post, Category, Tag, Comment
from .forms import PostForm, CommentForm
@method_decorator(cache_page(60 * 5), name='dispatch') # 缓存5分钟
class PostListView(ListView):
"""文章列表视图"""
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 10
def get_queryset(self):
queryset = Post.objects.published().with_counts()
# 分类过滤
category_slug = self.kwargs.get('category_slug')
if category_slug:
self.current_category = get_object_or_404(Category, slug=category_slug)
queryset = queryset.by_category(self.current_category)
# 标签过滤
tag_slug = self.kwargs.get('tag_slug')
if tag_slug:
self.current_tag = get_object_or_404(Tag, slug=tag_slug)
queryset = queryset.by_tag(self.current_tag)
# 搜索
query = self.request.GET.get('q')
if query:
self.search_query = query
queryset = queryset.search(query)
return queryset.prefetch_related('tags')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['categories'] = Category.objects.annotate(
post_count=models.Count('posts', filter=models.Q(posts__status='published'))
).filter(post_count__gt=0)
context['featured_posts'] = Post.objects.featured()[:5]
context['popular_tags'] = Tag.objects.annotate(
post_count=models.Count('posts')
).filter(post_count__gt=0).order_by('-post_count')[:20]
return context
class PostDetailView(DetailView):
"""文章详情视图"""
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
def get_queryset(self):
return Post.objects.select_related(
'author', 'author__profile', 'category'
).prefetch_related('tags', 'comments__author')
def get_object(self):
obj = super().get_object()
# 验证文章是否可访问
if obj.status != 'published':
if not self.request.user.is_authenticated:
raise Http404
if obj.author != self.request.user and not self.request.user.is_staff:
raise Http404
# 异步增加浏览量(不阻塞主请求)
obj.increment_views()
return obj
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
post = self.object
# 相关文章(相同分类或相同标签)
related_posts = Post.objects.published().filter(
models.Q(category=post.category) | models.Q(tags__in=post.tags.all())
).exclude(pk=post.pk).distinct()[:5]
# 上一篇/下一篇
try:
context['next_post'] = Post.objects.published().filter(
publish__gt=post.publish
).order_by('publish').first()
context['prev_post'] = Post.objects.published().filter(
publish__lt=post.publish
).order_by('-publish').first()
except Post.DoesNotExist:
pass
# 评论
context['comments'] = post.comments.filter(
active=True, parent=None
).select_related('author', 'author__profile').prefetch_related(
'replies__author', 'replies__author__profile'
)
context['comment_form'] = CommentForm()
context['related_posts'] = related_posts
return context
@login_required
def post_create(request):
"""创建文章"""
if request.method == 'POST':
form = PostForm(request.POST, request.FILES)
if form.is_valid():
with transaction.atomic():
post = form.save(commit=False)
post.author = request.user
post.save()
form.save_m2m() # 保存多对多字段(tags)
messages.success(request, f'文章《{post.title}》创建成功!')
return redirect(post.get_absolute_url())
else:
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form, 'action': '创建'})
@login_required
@require_POST
def add_comment(request, post_pk):
"""添加评论(AJAX)"""
post = get_object_or_404(Post, pk=post_pk, status='published', allow_comments=True)
form = CommentForm(request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.post = post
comment.author = request.user
parent_id = request.POST.get('parent_id')
if parent_id:
parent = get_object_or_404(Comment, pk=parent_id, post=post)
comment.parent = parent
comment.save()
return JsonResponse({
'success': True,
'comment_id': comment.pk,
'author': comment.author.username,
'body': comment.body,
'created': comment.created.strftime('%Y-%m-%d %H:%M'),
})
return JsonResponse({'success': False, 'errors': form.errors}, status=400)
附录:常见问题 FAQ
Q1:ORM 生成的 SQL 是否会有性能问题?
A: 会的,ORM 生成的 SQL 有时不够优化。建议:
-
使用
connection.queries查看执行的 SQL(调试模式):from django.db import connection
print(connection.queries) -
使用 Django Debug Toolbar(开发工具)
-
使用
.explain()分析查询计划 -
关键路径使用原生 SQL
Q2:信号和 save() 方法覆盖,哪个更好?
A: 各有适用场景:
-
重写 save() 方法:适合与模型本身强相关的逻辑(如自动生成 slug)
-
信号:适合跨模块的解耦逻辑(如用户创建时初始化其他数据)
适合放在 save() 中(与 Post 强相关)
class Post(models.Model):
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super().save(*args, **kwargs)适合放在信号中(跨模块,与 Post 弱相关)
@receiver(post_save, sender=Post)
def notify_on_publish(sender, instance, **kwargs):
# 通知功能与文章模型解耦
pass
Q3:中间件和视图装饰器有什么区别?
A:
-
中间件:对所有请求/响应生效,适合全局性处理(日志、限流等)
-
装饰器:只对特定视图生效,适合局部性处理(登录验证等)
中间件:全局生效
class GlobalAuthMiddleware:
def call(self, request):
# 对所有请求做认证
pass装饰器:只对特定视图生效
@login_required
def my_private_view(request):
pass
Q4:如何调试信号?
# 方法1:临时添加打印/日志
@receiver(post_save, sender=Post)
def my_handler(sender, instance, **kwargs):
import logging
logger = logging.getLogger(__name__)
logger.debug(f"post_save 信号触发:post_id={instance.pk}")
# 方法2:查看某个信号的所有接收者
from django.db.models.signals import post_save
from blog.models import Post
receivers = post_save.receivers
for receiver in receivers:
print(receiver)
# 方法3:使用 django.test.utils.override_settings 在测试中禁用信号
from unittest.mock import patch
with patch('blog.signals.send_email') as mock_email:
post = Post.objects.create(...)
mock_email.assert_called_once()
Q5:Django ORM 支持哪些数据库?
| 数据库 | 支持程度 |
|---|---|
| SQLite | 官方支持(开发环境默认) |
| PostgreSQL | 官方支持(推荐生产使用) |
| MySQL/MariaDB | 官方支持 |
| Oracle | 官方支持 |
| MS SQL Server | 第三方支持(django-mssql-backend) |
总结
恭喜你读完了这份完整的 Django 核心知识指南!让我们回顾一下三大核心主题:
🗄️ Django ORM
- 模型(Model) 是数据库表的 Python 表示,字段类型丰富
- QuerySet 是惰性求值、可链式调用的强大查询接口
- F 表达式 解决并发更新问题,Q 对象实现复杂逻辑查询
- select_related / prefetch_related 是解决 N+1 问题的关键
- 自定义 Manager/QuerySet 让代码更清晰可维护
🔗 Django 中间件
- 中间件是请求/响应管道中的处理节点
- 洋葱模型:请求从外到内,响应从内到外
- 函数式中间件是 Django 1.10+ 的推荐写法
- 合理利用中间件实现:日志、限流、认证、维护模式等
📡 Django 信号
- 信号是发布-订阅模式,实现组件解耦
- 内置信号覆盖:模型生命周期、请求/响应、认证
- 始终在
AppConfig.ready()中注册信号 - 注意:异常处理、事务钩子、防止无限递归
📌 学习建议:
- 用本文中的博客项目代码动手实践
- 使用 Django Debug Toolbar 观察 ORM 生成的 SQL
- 多写信号处理函数,体验解耦的优雅
- 阅读 Django 官方文档了解更多细节:https://docs.djangoproject.com/
Happy Coding! 🎉