Django ORM、中间件与信号 — 完全指南

🎯 目标读者 :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 内置中间件详解)

SecurityMiddleware(安全中间件)

SessionMiddleware(会话中间件)

[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 实战案例:各类中间件开发)

案例1:请求计时中间件

[案例2:IP 限流中间件](#案例2:IP 限流中间件)

案例3:用户活跃时间记录中间件

[案例4:API 版本控制中间件](#案例4:API 版本控制中间件)

案例5:请求日志中间件

案例6:维护模式中间件

案例7:多语言/国际化中间件

[第三部分: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 信号的实战案例)

案例1:自动创建用户资料(最经典的信号用法)

[案例2:文章发布时自动生成 SEO 信息](#案例2:文章发布时自动生成 SEO 信息)

案例3:评论系统的信号处理

案例4:文件清理信号

案例5:缓存失效信号

[4.6 信号的常见坑与最佳实践](#4.6 信号的常见坑与最佳实践)

[4.6.1 常见问题](#4.6.1 常见问题)

问题1:信号被多次触发

[问题2:信号处理中的数据库查询导致 N+1](#问题2:信号处理中的数据库查询导致 N+1)

问题3:信号中修改对象导致无限递归

问题4:信号中的异常处理

问题5:信号与事务的关系

[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() 方法覆盖,哪个更好?)

Q3:中间件和视图装饰器有什么区别?

Q4:如何调试信号?

[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 有两个非常重要的特性:

  1. 惰性求值(Lazy Evaluation):QuerySet 被创建时不会立即访问数据库,只有在真正需要数据时才会执行 SQL 查询。

  2. 可链式调用(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 用生活类比理解中间件

想象你去机场坐飞机:

  1. 进门 → 安检(检查你有没有危险物品)
  2. 安检通过 → 值机(登记你的信息)
  3. 值机完成 → 登机(进入飞机/视图)
  4. 下飞机 → 入境检查(检查护照)
  5. 检查通过 → 取行李(获取响应内容)

每个环节都是一个"中间件",它们在你到达目的地(视图函数)之前和之后做各种处理。

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.pyMIDDLEWARE 列表中添加它:

复制代码
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 为什么需要信号

考虑一个场景:用户注册后需要:

  1. 创建用户资料(UserProfile)
  2. 发送欢迎邮件
  3. 记录注册日志
  4. 赠送新人积分

不使用信号(耦合的做法):

复制代码
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 有时不够优化。建议:

  1. 使用 connection.queries 查看执行的 SQL(调试模式):

    from django.db import connection
    print(connection.queries)

  2. 使用 Django Debug Toolbar(开发工具)

  3. 使用 .explain() 分析查询计划

  4. 关键路径使用原生 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() 中注册信号
  • 注意:异常处理、事务钩子、防止无限递归

📌 学习建议

  1. 用本文中的博客项目代码动手实践
  2. 使用 Django Debug Toolbar 观察 ORM 生成的 SQL
  3. 多写信号处理函数,体验解耦的优雅
  4. 阅读 Django 官方文档了解更多细节:https://docs.djangoproject.com/

Happy Coding! 🎉

相关推荐
m0_684501982 小时前
如何利用 watchEffect 实现在线人数实时统计?Socket 与响应式结合
jvm·数据库·python
重庆若鱼文化创意2 小时前
高端包装设计公司哪家好,报价差异常藏在纸张和印刷工艺里。
人工智能·python
zhangchaoxies2 小时前
C#怎么使用全局Using C#global using全局引用怎么配置减少每个文件的using声明【语法】
jvm·数据库·python
m0_676544382 小时前
mysql执行预处理语句流程是怎样的_SQL执行优化解析
jvm·数据库·python
川石课堂软件测试2 小时前
AI如何赋能软件测试行业的发展
人工智能·python·功能测试·网络协议·单元测试·测试用例·prometheus
weixin_381288182 小时前
HTML5中Noscript标签在脚本禁用环境下的补救
jvm·数据库·python
Ares-Wang2 小时前
flask 》》内置HTMLParser
后端·python·flask
2401_837163892 小时前
PHP怎么写API接口_RESTful API基础写法介绍【介绍】
jvm·数据库·python
qq_413502022 小时前
PHP跨平台部署AI应用_Docker容器化方案【教程】
jvm·数据库·python