Django笔记
一、入门基础
1. 环境安装与项目创建
1.1 安装
# 创建虚拟环境(推荐)
python -m venv venv
# Windows 激活
venv\Scripts\activate
# Linux/Mac 激活
source venv/bin/activate
# 安装 Django + DRF 全家桶
pip install django==5.2
pip install djangorestframework
pip install djangorestframework-simplejwt # JWT 认证
pip install django-filter # 过滤
pip install psycopg2-binary # PostgreSQL 驱动
pip install django-cors-headers # 跨域(前后端分离时用)
依赖清单建议写入
requirements.txt,pip install -r requirements.txt一键安装。
1.2 创建项目和应用
# 创建项目(会在当前目录生成 djangoproject/ 文件夹)
django-admin startproject djangoproject
# 进入项目目录
cd djangoproject
# 创建应用(app)—— Django 的功能模块单元
python manage.py startapp info_manage
项目 vs 应用:项目是整个网站的容器,应用是可复用的功能模块。一个项目可以包含多个应用,一个应用也可以被多个项目复用。
1.3 数据库准备
# PostgreSQL 创建数据库
psql -U admin -d postgres
CREATE DATABASE test;
# 生成迁移文件(检测模型变化,生成 0001_initial.py)
python manage.py makemigrations
# 执行迁移(把迁移文件应用到数据库,建表/改表)
python manage.py migrate
# 创建超级用户(访问 admin 后台用)
python manage.py createsuperuser
# 启动开发服务器
python manage.py runserver
makemigrations vs migrate:前者是"拍快照"(生成迁移文件),后者是"执行快照"(改数据库)。改了模型必须先 makemigrations 再 migrate。
2. Django 架构 (MVT)
Django 采用 MVT 设计模式(Model-View-Template),对应 MVC 中的 M-V-C:
┌──────────────────────────────────────────────────────┐
│ Django MVT │
│ │
│ 用户请求 ──→ URLconf(路由) ──→ View(视图) │
│ │ │
│ ┌──────────┼──────────┐ │
│ ▼ ▼ ▼ │
│ Model Template Response │
│ (数据层) (模板层) (返回给浏览器) │
│ │ │
│ ▼ │
│ Database │
└──────────────────────────────────────────────────────┘
Model = 数据模型,ORM 映射到数据库表
View = 视图函数/类,处理业务逻辑(相当于 MVC 的 Controller)
Template = HTML 模板,渲染页面(相当于 MVC 的 View)
与 MVC 的对应关系:Django 的 View = MVC 的 Controller,Django 的 Template = MVC 的 View。只是换了个名字,本质一样。
3. 项目目录结构
djangoproject/ # 项目根目录
├── manage.py # 命令行入口(启动服务器、迁移等)
├── djangoproject/ # 项目配置包
│ ├── __init__.py
│ ├── settings.py # 全局配置(数据库、应用列表、DRF 配置等)
│ ├── urls.py # 项目级 URL 路由(总路由)
│ ├── wsgi.py # 部署入口(ASGI/WSGI)
│ └── asgi.py
└── info_manage/ # 应用目录
├── __init__.py
├── apps.py # 应用配置类
├── admin.py # Admin 后台注册
├── models/ # 数据模型(拆分到多文件)
│ ├── __init__.py # 统一导出所有模型
│ ├── BaseModel.py # 抽象基类
│ ├── User.py # 自定义用户
│ ├── InfoItem.py # 信息记录(核心表)
│ ├── Category.py # 分类
│ ├── Tag.py # 标签
│ ├── Attachment.py # 附件
│ └── Comment.py # 评论(自引用)
├── views.py # 视图(函数视图 + 视图集)
├── serializers.py # DRF 序列化器
├── permissions.py # 自定义权限
├── exceptions.py # 自定义异常处理
├── forms.py # ModelForm 表单
├── urls.py # MVT 路由(函数视图)
├── api_urls.py # DRF 路由(Router 自动生成)
└── templates/ # HTML 模板
模型可以拆分到
models/目录(包),每个模型一个文件。关键是__init__.py要 import +__all__导出,Django 才能发现。
4. settings.py 配置详解
settings.py 是整个项目的配置中心。以下按功能分组解释:
4.1 基础配置
import os
from pathlib import Path
# 项目根目录(settings.py 的上上一级)
BASE_DIR = Path(__file__).resolve().parent.parent
# 密钥(用于签名 session、CSRF token 等,生产环境必须保密)
SECRET_KEY = 'django-insecure-xxx'
# 调试模式:True 显示详细错误页,生产环境必须设为 False
DEBUG = True
# 允许访问的域名(DEBUG=True 时自动允许 localhost)
# 生产环境:ALLOWED_HOSTS = ['www.example.com']
ALLOWED_HOSTS = []
4.2 应用列表 (INSTALLED_APPS)
INSTALLED_APPS = [
# ---- Django 内置应用 ----
'django.contrib.admin', # 后台管理
'django.contrib.auth', # 认证系统
'django.contrib.contenttypes', # 内容类型框架
'django.contrib.sessions', # session 支持
'django.contrib.messages', # 消息框架
'django.contrib.staticfiles', # 静态文件管理
# ---- 第三方应用 ----
'rest_framework', # DRF
'rest_framework_simplejwt', # JWT 认证
'django_filters', # 过滤器
# ---- 自己的应用 ----
'info_manage',
]
新建的 app 必须加到
INSTALLED_APPS才能被 Django 识别。
4.3 中间件 (MIDDLEWARE)
中间件是请求/响应的"钩子",按顺序执行:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', # 安全头
'django.contrib.sessions.middleware.SessionMiddleware', # session 处理
'django.middleware.common.CommonMiddleware', # 通用处理
'django.middleware.csrf.CsrfViewMiddleware', # CSRF 防护
'django.contrib.auth.middleware.AuthenticationMiddleware', # 用户认证
'django.contrib.messages.middleware.MessageMiddleware', # 消息框架
'django.middleware.clickjacking.XFrameOptionsMiddleware', # 防点击劫持
]
请求 → Middleware1 → Middleware2 → ... → View → Middleware2 → Middleware1 → 响应
(正向处理) (反向处理)
4.4 数据库配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql', # 数据库引擎
'NAME': 'test', # 数据库名
'USER': 'admin', # 用户名
'PASSWORD': '123', # 密码
'HOST': 'localhost', # 主机
'PORT': '5432', # 端口
}
}
其他引擎:
django.db.backends.sqlite3(默认)、django.db.backends.mysql、django.db.backends.postgresql。
4.5 模板配置
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'], # 全局模板目录
'APP_DIRS': True, # 自动在 app 的 templates/ 下找模板
'OPTIONS': {
'context_processors': [
# 以下四个是 Django 默认的上下文处理器,提供模板变量
'django.template.context_processors.request', # request 变量
'django.contrib.auth.context_processors.auth', # user 变量
'django.contrib.messages.context_processors.messages', # messages
],
},
},
]
4.6 认证配置
# 自定义用户模型(必须在第一次 migrate 之前设置!)
AUTH_USER_MODEL = "info_manage.User"
# 密码校验器(注册时检查密码强度)
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, # 不能和用户名太像
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, # 最短长度
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, # 常见密码
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, # 不能纯数字
]
⚠️ AUTH_USER_MODEL 必须在第一次
migrate之前配置,否则需要删库重建。
4.7 国际化与时区
LANGUAGE_CODE = 'en-us' # 'zh-hans' 表示简体中文
TIME_ZONE = 'UTC' # 'Asia/Shanghai' 表示东八区
USE_I18N = True # 启用国际化
USE_TZ = True # 启用时区(数据库存 UTC,展示时转换)
4.8 静态文件与媒体文件
# 静态文件(CSS/JS/图片,开发时 Django 自动服务)
STATIC_URL = 'static/'
# STATIC_ROOT = BASE_DIR / 'staticfiles' # 生产环境 collectstatic 收集目录
# 媒体文件(用户上传的文件)
MEDIA_ROOT = os.path.join(BASE_DIR, "media") # 文件存储根目录
MEDIA_URL = "/media/" # URL 前缀
静态文件 vs 媒体文件:静态文件是开发者写的(CSS/JS),媒体文件是用户上传的(头像/附件)。
4.9 DRF 全局配置
REST_FRAMEWORK = {
# 自定义异常处理:统一错误返回格式
'EXCEPTION_HANDLER': 'info_manage.exceptions.custom_exception_handler',
# 分页:按页码翻页
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10, # 每页 10 条
# 认证(识别"你是谁"):按顺序尝试,取第一个成功的
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication', # JWT(前后端分离)
'rest_framework.authentication.SessionAuthentication', # Session(浏览器)
],
# 权限(决定"你能不能进"):所有 API 默认要求登录
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
# 过滤 / 搜索 / 排序后端
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend', # 字段精确过滤
'rest_framework.filters.SearchFilter', # 模糊搜索
'rest_framework.filters.OrderingFilter', # 排序
],
}
# JWT 令牌有效期
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(hours=2), # 访问令牌 2 小时
'REFRESH_TOKEN_LIFETIME': timedelta(days=7), # 刷新令牌 7 天
}
5. manage.py 常用命令
| 命令 | 作用 |
|---|---|
python manage.py runserver |
启动开发服务器(默认 8000 端口) |
python manage.py runserver 8080 |
指定端口 |
python manage.py makemigrations |
生成迁移文件 |
python manage.py migrate |
执行迁移 |
python manage.py createsuperuser |
创建超级管理员 |
python manage.py shell |
进入 Django 交互式 shell |
python manage.py collectstatic |
收集静态文件到 STATIC_ROOT |
python manage.py startapp myapp |
创建新应用 |
python manage.py dbshell |
进入数据库命令行 |
二、模型层 (Model)
1. ORM 概念
ORM(Object-Relational Mapping)把数据库表映射为 Python 类:
Python 类 → 数据库表
类的属性 → 表的列
类的实例 → 表的一行数据
obj.save() → INSERT/UPDATE
obj.delete() → DELETE
Model.objects.all() → SELECT * FROM table
好处:不用写 SQL,用 Python 语法操作数据库,自动防 SQL 注入。
2. 常用字段类型
from django.db import models
# 基础类型
title = models.CharField(max_length=200) # VARCHAR(200),必须指定 max_length
content = models.TextField() # TEXT,不限长度
count = models.IntegerField() # INTEGER
price = models.FloatField() # DOUBLE
is_active = models.BooleanField(default=True) # BOOLEAN
pub_date = models.DateField() # DATE (2025-01-01)
pub_time = models.DateTimeField() # DATETIME (2025-01-01 10:00:00)
# 特殊类型
code = models.SlugField(max_length=50) # 只允许字母、数字、下划线、连字符
email = models.EmailField() # 自动校验邮箱格式
url_field = models.URLField() # 自动校验 URL
file = models.FileField(upload_to="attachments/") # 文件上传
image = models.ImageField(upload_to="avatars/") # 图片上传(会校验是否为图片)
| 字段类型 | Python 类型 | 数据库类型 | 说明 |
|---|---|---|---|
| CharField | str | VARCHAR | 短文本,必须 max_length |
| TextField | str | TEXT | 长文本,不限长度 |
| IntegerField | int | INTEGER | 整数 |
| FloatField | float | DOUBLE | 浮点数 |
| BooleanField | bool | BOOLEAN | 布尔值 |
| DateField | date | DATE | 日期 |
| DateTimeField | datetime | DATETIME | 日期+时间 |
| FileField | str | VARCHAR | 文件路径 |
| SlugField | str | VARCHAR | URL 友好字符串 |
3. 字段常用参数
title = models.CharField(
max_length=200, # 最大长度(CharField 必填)
verbose_name="标题", # 人类可读名称(Admin 后台显示)
default="", # 默认值
blank=True, # 表单允许为空(Django 层校验)
null=True, # 数据库允许 NULL(数据库层)
unique=True, # 唯一约束
choices=[ # 枚举选项
('draft', '草稿'),
('published', '已发布'),
],
help_text="请输入标题", # 帮助提示
editable=False, # 不可编辑(不在表单/Admin 中出现)
)
blank vs null:
null=True:数据库允许存 NULLblank=True:表单验证允许为空- 常见组合:字符串字段用
blank=True, default=""(不要用 null=True,会导致 NULL 和空字符串共存的问题)
choices 枚举(推荐写法)
class InfoItem(BaseModel):
class Status(models.TextChoices):
DRAFT = "draft", "草稿"
PUBLISHED = "published", "已发布"
ARCHIVED = "archived", "已归档"
status = models.CharField(
max_length=20,
choices=Status.choices, # 用 TextChoices 自动生成
default=Status.DRAFT,
)
TextChoices是 Django 3+ 的枚举类。每个元组(值, 标签)—— 值存数据库,标签显示给用户。代码里用InfoItem.Status.DRAFT引用,比硬编码字符串安全。
4. 时间字段特殊参数
created_at = models.DateTimeField(auto_now_add=True) # 记录创建时自动设为当前时间,之后不再变
updated_at = models.DateTimeField(auto_now=True) # 每次 .save() 时自动更新为当前时间
| 参数 | 作用 | 典型用途 |
|---|---|---|
auto_now_add=True |
创建时自动设当前时间,之后不可修改 | created_at |
auto_now=True |
每次 save 时自动更新为当前时间 | updated_at |
⚠️ 用了
auto_now/auto_now_add的字段,在表单和 Admin 中默认不可编辑(editable=False)。
5. 关系字段
5.1 三种关系类型
┌──────────────┐ ┌──────────────┐
│ InfoItem │ FK │ Category │
│ (信息记录) │◄────────│ (分类) │
│ │ 多对一 │ │
└──────────────┘ └──────────────┘
┌──────────────┐ M2M ┌──────────────┐
│ InfoItem │◄───────►│ Tag │
│ (信息记录) │ 多对多 │ (标签) │
└──────────────┘ └──────────────┘
│
│ 中间表(自动生成):info_manage_infoitem_tags
│ infoitem_id | tag_id
▼
┌──────────────┐ O2O ┌──────────────┐
│ User │◄────────│ UserProfile │
│ (用户) │ 一对一 │ (扩展信息) │
└──────────────┘ └──────────────┘
5.2 ForeignKey(外键 / 多对一)
category = models.ForeignKey(
"Category", # 关联的目标模型(字符串引用,避免循环 import)
on_delete=models.PROTECT, # 被引用方删除时的行为(必填)
related_name="items", # 反向查询名:category.items.all() 查所有关联记录
verbose_name="分类",
null=True, # 允许不选分类
blank=True,
)
| 参数 | 作用 |
|---|---|
| 第一个参数 | 目标模型名,用字符串 "Category" 避免循环导入 |
on_delete |
必填,被引用方删除时的行为(见下文) |
related_name |
反向查询名。不设则默认为 模型名小写_set |
to_field |
关联目标的哪个字段(默认主键 id) |
related_name 是什么:设了
related_name="items"后,category.items.all()就能查这个分类下的所有记录。不设则用category.infoitem_set.all()。
5.3 ManyToManyField(多对多)
tags = models.ManyToManyField(
"Tag",
blank=True, # 允许不选标签
verbose_name="标签",
# through="ItemTag", # 自定义中间表(需要额外字段时才用)
)
M2M 会自动生成中间表(表名:
app名_模型A_模型B小写)。中间表只有两列外键,不需要手动建。
5.4 OneToOneField(一对一)
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="profile",
)
一对一本质是 ForeignKey + unique=True。常用于扩展已有模型(如给 User 加 phone/department 字段)。
6. on_delete 级联选项
| 选项 | 效果 | 适用场景 |
|---|---|---|
CASCADE |
删除被引用方 → 自动删除引用方 | 子记录跟随父记录(附件、评论) |
PROTECT |
有引用时禁止删除被引用方 | 主数据不能丢(分类、用户) |
SET_NULL |
删除被引用方 → 引用方外键设为 NULL | 可选关联(需 null=True) |
SET_DEFAULT |
删除被引用方 → 引用方外键设为默认值 | 有默认值的字段(需 default=xxx) |
DO_NOTHING |
什么都不做 | 不推荐,会导致数据库错误 |
RESTRICT |
类似 PROTECT,但允许级联链中的间接删除 | 比 PROTECT 更灵活 |
# 分类不能删(有记录在用)
category = models.ForeignKey("Category", on_delete=models.PROTECT)
# 记录删了,附件也删
info_item = models.ForeignKey("InfoItem", on_delete=models.CASCADE)
# 评论人不能删(有评论在引用)
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
7. Meta 内部类
class Meta:
verbose_name = "信息记录" # 单数名称(Admin 后台显示)
verbose_name_plural = verbose_name # 复数名称
ordering = ["-created_at"] # 默认排序(- 表示倒序)
abstract = True # 抽象类,不建表(供继承)
# db_table = 'my_table' # 自定义表名
# unique_together = [['field1', 'field2']] # 联合唯一约束
# indexes = [models.Index(fields=['title'])] # 数据库索引
| 参数 | 作用 |
|---|---|
ordering |
默认排序,- 前缀表示倒序 |
abstract=True |
抽象基类,不建表 |
verbose_name |
人类可读名 |
db_table |
自定义表名 |
unique_together |
联合唯一约束 |
indexes |
添加数据库索引 |
8. 模型继承(抽象基类)
# BaseModel.py — 抽象基类,所有模型共用这两个时间字段
class BaseModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
abstract = True # 关键:不建表,只供继承
# InfoItem.py — 继承 BaseModel,自动获得 created_at 和 updated_at
class InfoItem(BaseModel):
title = models.CharField(max_length=200)
# ...自带 created_at / updated_at
三种继承方式:
- 抽象基类(
abstract=True):父类不建表,子类继承字段。最常用。- 多表继承(不设 abstract):父类和子类各建一张表,用隐式 OneToOne 关联。
- 代理模型(
proxy=True):不建表,只改行为不改字段。
9. str 方法
def __str__(self):
return self.title # 在 Admin 后台 / print 时显示标题而不是 "InfoItem object(1)"
每个 Model 建议都写
__str__,否则在 Admin 和 shell 中显示为ModelName object(1),难以区分。
10. 数据库迁移
# 1. 改了 models.py
# 2. 生成迁移文件
python manage.py makemigrations
# 3. 执行迁移
python manage.py migrate
# 查看迁移状态
python manage.py showmigrations
# 查看迁移会执行什么 SQL(不执行)
python manage.py sqlmigrate info_manage 0001
迁移文件(
migrations/0001_initial.py)应该提交到 Git。不要手动改迁移文件,改了模型重新 makemigrations 即可。
三、URL 路由
1. path() 与 re_path()
from django.urls import path, re_path, include
# path():简单路由,支持转换器
path('items/', views.item_list),
path('items/<int:pk>/', views.item_detail), # <int:pk> 匹配数字
path('items/<str:slug>/', views.item_by_slug), # <str:slug> 匹配字符串
# re_path():正则路由(复杂匹配时用)
re_path(r'^items/(?P<pk>\d+)/edit/$', views.item_edit),
path() 转换器
| 转换器 | 匹配 | 示例 URL | 视图接收 |
|---|---|---|---|
<int:pk> |
数字 | /items/42/ |
pk=42(int 类型) |
<str:name> |
字符串 | /items/hello/ |
name='hello' |
<slug:slug> |
字母数字下划线连字符 | /items/hello-world/ |
slug='hello-world' |
<uuid:uid> |
UUID | /items/xxx-xxx/ |
uid=UUID对象 |
<path:filepath> |
含斜杠的路径 | /files/a/b.txt |
filepath='a/b.txt' |
2. include() 分发
# 项目级 urls.py(总路由)
urlpatterns = [
path('admin/', admin.site.urls),
path('items/', include('info_manage.urls')), # MVT 路由
path('api/', include('info_manage.api_urls')), # DRF 路由
path('accounts/', include('django.contrib.auth.urls')), # 内置认证
]
include()把 URL 前缀 + 子路由拼接。path('items/', include(...))表示所有以items/开头的 URL 交给info_manage/urls.py处理。
3. name 与 reverse 反向解析
# urls.py
path('items/<int:pk>/', views.item_detail, name='item_detail')
# 视图/模板中用 name 反向解析出 URL
from django.urls import reverse
url = reverse('item_detail', kwargs={'pk': 42}) # → '/items/42/'
# 模板中
{% url 'item_detail' item.id %}
为什么要 name:URL 硬编码后改路径要改很多地方。用 name 反向解析,改 URL 不用改代码。
四、视图层 - MVT 函数视图
1. request 对象
def my_view(request):
request.method # 'GET' / 'POST'
request.GET.get('search', '') # GET 参数
request.POST.get('title', '') # POST 参数
request.FILES.get('file') # 上传的文件
request.user # 当前登录用户
request.path # 当前 URL 路径
request.is_ajax() # 是否 AJAX 请求
2. render / redirect / get_object_or_404
from django.shortcuts import render, redirect, get_object_or_404
def item_list(request):
items = InfoItem.objects.all()
# render(请求对象, 模板路径, 上下文字典)
return render(request, 'info_manage/item_list.html', {'items': items})
def item_delete(request, pk):
# 找不到记录返回 404,不用手写 try-except
item = get_object_or_404(InfoItem, pk=pk)
item.delete()
return redirect('item_list') # 重定向到 name='item_list' 的 URL
3. login_required 装饰器
from django.contrib.auth.decorators import login_required
@login_required # 未登录用户自动跳转到 settings.LOGIN_URL
def item_list(request):
...
未登录用户访问被
@login_required保护的视图时,自动重定向到/accounts/login/?next=/items/。
4. 函数视图 CRUD 实例
# 列表(搜索 + 关联查询优化)
@login_required
def item_list(request):
title_search = request.GET.get('search', '')
category_id = request.GET.get('category', '')
# select_related: JOIN 查询 FK(减少查询次数)
# prefetch_related: 单独查 M2M(避免 N+1 问题)
items = InfoItem.objects.select_related('category', 'created_by').prefetch_related('tags')
if title_search:
items = items.filter(title__icontains=title_search) # 模糊搜索
if category_id:
items = items.filter(category_id=category_id)
return render(request, 'info_manage/item_list.html', {'items': items})
# 新增
@login_required
def item_create(request):
if request.method == "POST":
form = InfoItemForm(request.POST)
if form.is_valid():
item = form.save(commit=False) # 不立即保存,先补充字段
item.created_by = request.user
item.updated_by = request.user
item.save() # 保存到数据库
form.save_m2m() # 保存多对多关系
return redirect('item_list')
else:
form = InfoItemForm()
return render(request, 'info_manage/item_form.html', {'form': form})
# 编辑
@login_required
def item_edit(request, pk):
item = get_object_or_404(InfoItem, pk=pk)
if request.method == "POST":
form = InfoItemForm(request.POST, instance=item) # instance 指定要修改的记录
if form.is_valid():
item = form.save(commit=False)
item.updated_by = request.user
item.save()
form.save_m2m()
return redirect('item_list')
else:
form = InfoItemForm(instance=item)
return render(request, 'info_manage/item_form.html', {'form': form})
# 删除
@login_required
def item_delete(request, pk):
item = get_object_or_404(InfoItem, pk=pk)
if request.method == "POST":
item.delete()
return redirect('item_list')
return render(request, 'info_manage/item_confirm_delete.html', {'item': item})
ORM 常用查询方法
# 基础查询
InfoItem.objects.all() # 全部
InfoItem.objects.get(pk=1) # 查单条(不存在报错)
InfoItem.objects.filter(status='published') # 过滤(返回 QuerySet)
InfoItem.objects.exclude(status='draft') # 排除
InfoItem.objects.order_by('-created_at') # 排序
InfoItem.objects.first() # 第一条
InfoItem.objects.count() # 计数
InfoItem.objects.exists() # 是否存在
# 字段查找(双下划线语法)
# ── 大小比较(最容易写错:strict 严格 / inclusive 含等号)──
InfoItem.objects.filter(priority__gt=5) # > 严格大于
InfoItem.objects.filter(priority__gte=5) # >= 大于等于(含 5)
InfoItem.objects.filter(priority__lt=5) # < 严格小于(不含 5!)
InfoItem.objects.filter(priority__lte=5) # <= 小于等于(含 5)
# ⚠️ 易错:想要"小于 5"用的是 __lt(不含等号),不是 __lte;
# __lte 是"小于等于"。把 <= 直觉直接套会多包含边界值。
InfoItem.objects.filter(title__icontains='hello') # 模糊搜索(不区分大小写)
InfoItem.objects.filter(title__contains='hello') # 包含(区分大小写)
InfoItem.objects.filter(created_at__gte='2025-01-01')# 大于等于
InfoItem.objects.filter(created_at__year=2025) # 按年
InfoItem.objects.filter(id__in=[1, 2, 3]) # 在列表中(空列表→查不到任何记录)
InfoItem.objects.filter(title__isnull=True) # 为 NULL(NULL 用 __isnull,别用 =None)
# 关联查询(跨表,用 related_name)
category.items.all() # 反查:该分类下所有记录
item.tags.all() # 正查:该记录的所有标签
item.attachments.all() # 反查:该记录的所有附件
Comment.objects.filter(parent__isnull=True) # parent 为 NULL 的一级评论
# 关联查询优化
InfoItem.objects.select_related('category') # FK 用 JOIN(1 次查询)
InfoItem.objects.prefetch_related('tags') # M2M 单独查(2 次查询,避免 N+1)
# 聚合与分组
from django.db.models import Count
InfoItem.objects.aggregate(total=Count('id')) # {'total': 10}
InfoItem.objects.values('status').annotate(count=Count('id')) # GROUP BY
# 结果:[{'status': 'draft', 'count': 5}, {'status': 'published', 'count': 3}]
# F 对象(引用其他字段的值)
from django.db.models import F
InfoItem.objects.update(priority=F('priority') + 1) # 所有记录 priority +1
# Q 对象(复杂 OR/AND 条件)
from django.db.models import Q
InfoItem.objects.filter(Q(title__icontains='hello') | Q(description__icontains='world'))
ORM 增删改操作
上一节全是「查」,这里补「增 / 改 / 删」。三者最终都对应 SQL 的
INSERT/UPDATE/DELETE。
1. 创建(Create / INSERT)
| 方式 | 写法 | 说明 | 注意 |
|---|---|---|---|
| 一行创建并落库 | InfoItem.objects.create(title=..., created_by=user) |
返回模型实例,最常用 | 外键直接传对象 |
先实例化再 save() |
item = InfoItem(...); item.save() |
适合 save 前做额外处理(算字段、调接口) | 默认 UPDATE 所有字段 |
| 强制插入 | item.save(force_insert=True) |
即使对象已有 pk 也 INSERT | 不 UPDATE |
| 有则返、无则建 | get_or_create(查询条件, defaults={...}) |
返回 (对象, 是否新建) |
避免重复插入 |
| 有则更新、无则建(upsert) | update_or_create(查询条件, defaults={...}) |
匹配到就更新 defaults 字段 | 同上 |
| 批量插入 | bulk_create([obj1, obj2, ...]) |
一次 INSERT 多行,比循环 create() 快 |
⚠️ 不触发 save / signal / auto_now |
注意:
create()/save()不会自动跑模型里的clean()校验(除非显式调用instance.full_clean())。Django 的"校验"主要在 Form / Serializer 层,不在 ORM 写入层。
2. 更新(Update / UPDATE)
| 方式 | 写法 | 说明 | 是否触发 save / signal / auto_now |
|---|---|---|---|
改属性后 save() |
item.status=...; item.save() |
更新单条 | ✅ 是 |
| 只更新指定字段 | item.save(update_fields=["status"]) |
减少 SQL 写入量,推荐 | ✅ 是(仅该字段) |
| 强制更新 | item.save(force_update=True) |
对象必须有 pk | ✅ 是 |
| 批量更新 | QuerySet.update(status='published') |
直接生成 SQL | ❌ 否(updated_at 不自动变) |
| F 对象原子自增 | update(priority=F('priority') + 1) |
数据库内算,避免"先读后写"竞态 | ❌ 否 |
| 批量更新一组对象 | bulk_update(objs, ['status']) |
Django 2.2+,比逐条 save 快 |
❌ 否 |
obj.save() |
QuerySet.update() |
|
|---|---|---|
| 作用范围 | 单条对象 | 整个 QuerySet(批量) |
| 触发 save() / signal | ✅ 是 | ❌ 否 |
| 更新 auto_now 字段 | ✅ 是(updated_at 自动变) | ❌ 否 |
| 适用场景 | 改单条 + 有业务逻辑 | 批量改状态 / 批量刷数据 |
经验法则:改单条且要跑校验/钩子用
save();批量改状态用update()(高效,但 updated_at 等时间字段要自己维护)。
3. 删除(Delete / DELETE)
| 方式 | 写法 | 说明 | 注意 |
|---|---|---|---|
| 单条删除 | item = InfoItem.objects.get(pk=1); item.delete() |
先加载对象再删 | 走到实例的 delete() 钩子 |
| 批量删除 | InfoItem.objects.filter(status='archived').delete() |
直接生成 SQL | ⚠️ 不调自定义 delete() 钩子 |
on_delete 级联 |
CASCADE / PROTECT / SET_NULL |
定义外键删除行为 | SET_NULL 需字段 null=True |
get().delete()** 与filter().delete()的区别**:
Model.objects.filter(pk=1).delete()是批量删除,直接生成 SQL,信号仍会发,但不会调用你在模型上重写的delete()方法。Model.objects.get(pk=1).delete()会先加载对象再删,会走到实例的delete()逻辑。- 所以:想用自定义的
delete()钩子(比如删文件),要get()出来再.delete(),别用filter().delete()。
软删除(实战常用):很多项目不真删,而是加
is_deleted = BooleanField(default=False),删除改成update(is_deleted=True),查询时.filter(is_deleted=False)。好处:可恢复、留审计痕迹。
ORM 易错点与避坑
下面这些坑都和"把 Python / SQL 直觉直接套到 ORM"有关,写业务时最容易踩。
① 大小比较运算符(strict 严格 / inclusive 含等号)
| Python 运算符 | ORM 写法 | 含义 | 易错提醒 |
|---|---|---|---|
> |
field__gt |
严格大于 | |
>= |
field__gte |
大于等于(含边界) | |
< |
field__lt |
严格小于(不含边界) | ⚠️ 想要"小于"用这个,不是 __lte |
<= |
field__lte |
小于等于(含边界) |
⚠️ 想要"小于 5"用
__lt(不含等号),不是__lte;__lte是"小于等于"。把<=直觉直接套会多包含边界值。
② 条件组合逻辑(AND / OR / exclude)
| 写法 | 实际逻辑 | 易错点 |
|---|---|---|
filter(a=1, b=2) |
a=1 AND b=2 |
多参数不是 OR |
| `filter(Q(a=1) | Q(b=2))` | a=1 OR b=2 |
exclude(a=1, b=2) |
排除"同时满足 a=1 且 b=2"的行 | 不是"排除 a=1 或 b=2" |
③ 取值 / 判空 / 计数 / 范围
| 场景 | 推荐写法 | 坑 |
|---|---|---|
| 取单条(可能没有) | filter(pk=1).first() → None |
get() 查不到抛 DoesNotExist |
| 取单条(必须有) | get_object_or_404() |
同上 |
| 判 NULL | field__isnull=True / False |
别用 =None |
动态 __in |
先判空再 filter(id__in=ids) |
id__in=[] 返回空结果(不是"不过滤") |
| 只计数 | .count() |
len() 加载全表进内存 |
| 只判断有无 | .exists() |
同上 |
| 日期区间 | field__range=(a, b) |
含 a 和 b(BETWEEN),不像 Python 切片右开 |
④ 关联查询优化
| 关系类型 | 用哪个 | 说明 |
|---|---|---|
| FK / O2O | select_related('x') |
JOIN 一次查完 |
| M2M / 反向 FK | prefetch_related('x') |
单独查,避免 N+1 |
| 用反 | 无效 / 低效 | 对 M2M 用 select_related 不起作用 |
⑤ 返回值类型 / 执行时机 / 批量钩子
| 写法 | 返回 / 行为 | 坑 |
|---|---|---|
values() / values_list() |
字典 / 元组,非模型对象 | 不能调模型方法、不能 .save() |
QuerySet(未求值) |
惰性,不立即查库 | 遍历 / 切片 / count 才触发 SQL |
queryset[-1] |
报错 | 不支持负索引,取末尾用 .last() |
QuerySet.update() / delete() |
批量 SQL | 不触发 save() / signal(要钩子需逐条 get()) |
五、表单层 (Form)
1. ModelForm
from django import forms
from .models import InfoItem
class InfoItemForm(forms.ModelForm):
class Meta:
model = InfoItem
fields = ['title', 'category', 'tags', 'status', 'description', 'priority', 'published_at']
# fields = '__all__' # 所有字段
# exclude = ['created_by'] # 排除某些字段
# 自定义字段 widget(覆盖模型默认的表单控件)
description = forms.CharField(
widget=forms.Textarea(attrs={"rows": 4}), # 多行文本框
required=False,
)
published_at = forms.DateTimeField(
widget=forms.DateTimeInput(attrs={"type": "datetime-local"}), # HTML5 日期选择器
required=False,
)
| Meta 属性 | 作用 |
|---|---|
model |
关联的模型类 |
fields |
包含哪些字段(列表或 '__all__') |
exclude |
排除哪些字段 |
widgets |
自定义控件 |
labels |
字段标签 |
2. save(commit=False)
form = InfoItemForm(request.POST)
if form.is_valid():
item = form.save(commit=False) # 创建对象但不保存到数据库
item.created_by = request.user # 补充额外字段
item.save() # 现在才保存
form.save_m2m() # 保存多对多关系(commit=False 后必须手动调)
为什么需要 commit=False:有些字段(如 created_by)不来自表单,而是来自 request.user。先创建对象不保存,补充字段后再一起保存。
六、模板层 (Template)
1. 模板继承
<!-- base.html — 基础模板 -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}默认标题{% endblock %}</title>
</head>
<body>
<nav>导航栏</nav>
{% block content %}{% endblock %}
<footer>页脚</footer>
</body>
</html>
<!-- item_list.html — 子模板 -->
{% extends "base.html" %}
{% block title %}信息列表{% endblock %}
{% block content %}
<h1>信息列表</h1>
{% for item in items %}
<p>{{ item.title }}</p>
{% endfor %}
{% endblock %}
2. 常用模板标签
{% extends "base.html" %} {# 继承基础模板 #}
{% include "header.html" %} {# 包含子模板 #}
{% for item in items %}...{% endfor %} {# 循环 #}
{% if user.is_authenticated %}...{% endif %} {# 条件判断 #}
{% url 'item_detail' item.id %} {# URL 反向解析 #}
{% csrf_token %} {# CSRF 令牌(POST 表单必加)#}
{% load static %} {# 加载静态文件标签库 #}
{% static 'css/style.css' %} {# 引用静态文件 #}
{{ forloop.counter }} {# 当前循环序号(从1开始)#}
{{ forloop.last }} {# 是否最后一次循环 #}
{% empty %} {# for 循环为空时显示 #}
3. 常用过滤器
{{ value|default:"默认值" }} {# 为空时显示默认值 #}
{{ value|length }} {# 长度 #}
{{ value|date:"Y-m-d H:i" }} {# 日期格式化 #}
{{ value|truncatewords:30 }} {# 截断为30个词 #}
{{ value|truncatechars:50 }} {# 截断为50个字符 #}
{{ value|safe }} {# 不转义 HTML(信任内容时才用)#}
{{ value|add:10 }} {# 加法 #}
{{ value|upper }} {# 转大写 #}
{{ value|lower }} {# 转小写 #}
{{ value|join:", " }} {# 列表拼接 #}
七、DRF 入门
1. MVT vs DRF 对比
| 维度 | MVT | DRF |
|---|---|---|
| 返回格式 | HTML 页面 | JSON 数据 |
| 前后端 | 模板渲染,前后端不分离 | 前端独立(Vue/React),调 API |
| 路由 | 手动写 path() | Router 自动生成 |
| 视图 | 函数 / 类视图 | ViewSet(一个类搞定 CRUD) |
| 数据处理 | Form + 模板 | Serializer(JSON ↔ Python 对象) |
| 认证 | Session | JWT / Session |
| 表单 | ModelForm | Serializer |
| 模板 | 必须 | 不需要 |
MVT 流程:
请求 → URL → View(函数) → Model(数据库) → Template(HTML) → 响应 HTML
DRF 流程:
请求 → URL → ViewSet → Serializer → Model(数据库) → JSON 响应
↑
前端(Vue/React) ←→ JSON
2. DRF 核心组件
┌──────────────────────────────────────────────┐
│ DRF 架构 │
│ │
│ 请求 → Router(路由) → ViewSet(视图集) │
│ │ │
│ ┌────────────┼────────────┐ │
│ ▼ ▼ ▼ │
│ Serializer Permission Filter │
│ (序列化器) (权限) (过滤) │
│ │ │
│ ▼ │
│ Model(数据库) │
│ │ │
│ ▼ │
│ JSON Response │
└──────────────────────────────────────────────┘
Router → 自动把 ViewSet 映射成 RESTful URL
ViewSet → 一个类封装 CRUD + 自定义动作
Serializer → 模型对象 ↔ JSON 互转 + 数据校验
Permission → 控制谁能访问
Filter → 搜索/过滤/排序
八、序列化器 (Serializer)
1. ModelSerializer
from rest_framework import serializers
from .models import InfoItem
class InfoItemSerializer(serializers.ModelSerializer):
class Meta:
model = InfoItem
fields = '__all__' # 序列化所有字段
read_only_fields = ['created_by', 'updated_by'] # 这两个字段前端只读,后端自动填
Meta 常用参数
| 参数 | 作用 |
|---|---|
model |
关联的模型类 |
fields |
包含哪些字段。'__all__' 表示全部 |
exclude |
排除哪些字段(和 fields 二选一) |
read_only_fields |
只读字段(前端传了也忽略,后端自动填) |
extra_kwargs |
给字段额外配置 |
depth |
外键展开深度(不推荐,用 SerializerMethodField 代替) |
fields vs exclude:二选一,不能同时用。推荐
fields显式列出(即使改了模型也不会意外暴露字段)。
read_only_fields 的作用:防止前端伪造数据。比如
created_by应该从 JWT 取当前用户,不能让前端传。
2. SerializerMethodField(自定义计算字段)
class AttachmentSerializer(serializers.ModelSerializer):
file_url = serializers.SerializerMethodField()
class Meta:
model = Attachment
fields = '__all__'
def get_file_url(self, obj):
"""obj 是当前序列化的模型实例"""
request = self.context.get('request')
if obj.file and request:
return request.build_absolute_uri(obj.file.url) # 拼完整 URL
return None
SerializerMethodField不是数据库字段,而是通过方法计算出来的值。方法名必须是get_字段名。
3. source 跨表取值
class CommentSerializer(serializers.ModelSerializer):
# 不返回 author 的 ID,而是直接返回用户名
author_name = serializers.CharField(source='author.username', read_only=True)
class Meta:
model = Comment
fields = ['id', 'author', 'author_name', 'content', ...]
source='author.username'告诉 DRF:从obj.author.username取值。DRF 会自动跨表查询。
4. 嵌套序列化(限深一层)
class CommentSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source='author.username', read_only=True)
replies = serializers.SerializerMethodField()
class Meta:
model = Comment
fields = ['id', 'info_item', 'author', 'author_name', 'content',
'parent', 'replies', 'created_at']
read_only_fields = ['author']
def get_replies(self, obj):
"""只对一级评论查回复,回复不再嵌套(限深一层,避免无限递归)"""
if obj.parent is not None:
return [] # 当前是回复 → 不再查子回复
replies = obj.replies.all() # 当前是一级评论 → 查所有回复
return CommentSerializer(replies, many=True, context=self.context).data
为什么要限深:如果不加
if obj.parent is not None: return [],序列化器会无限递归——查评论的回复的回复的回复……直到内存溢出。限深一层是性能和功能的平衡。
5. context 传递
# ViewSet 中调用时传 context
serializer = CommentSerializer(comments, many=True, context={'request': request})
# Serializer 中取 context
request = self.context.get('request')
context用于在 ViewSet 和 Serializer 之间传递数据。最常见的是传request,用于拼完整 URL、判断当前用户等。
九、视图集 (ViewSet)
1. ModelViewSet 源码解读
ModelViewSet 继承链:
ModelViewSet
├── CreateModelMixin → create() → POST /api/items/
├── RetrieveModelMixin → retrieve() → GET /api/items/{id}/
├── UpdateModelMixin → update() → PUT /api/items/{id}/
├── DestroyModelMixin → destroy() → DELETE /api/items/{id}/
└── ListModelMixin → list() → GET /api/items/
└── GenericViewSet
└── GenericAPIView
├── queryset → 数据集
├── serializer_class → 序列化器
├── filter_backends → 过滤后端
├── pagination_class → 分页
└── ...
6 个 Mixin 各自带一个 CRUD 方法。
ModelViewSet把它们全组合在一起,所以一个类就实现了完整的 CRUD。
CRUD 方法 → HTTP 映射
| Mixin | 方法 | HTTP | URL | 作用 |
|---|---|---|---|---|
| ListModelMixin | list() |
GET | /api/items/ |
列表 |
| CreateModelMixin | create() |
POST | /api/items/ |
新增 |
| RetrieveModelMixin | retrieve() |
GET | /api/items/{id}/ |
详情 |
| UpdateModelMixin | update() |
PUT | /api/items/{id}/ |
全量修改 |
| UpdateModelMixin | partial_update() |
PATCH | /api/items/{id}/ |
部分修改 |
| DestroyModelMixin | destroy() |
DELETE | /api/items/{id}/ |
删除 |
create() 源码简化版
# CreateModelMixin.create() 的核心逻辑(简化):
def create(self, request, *args, **kwargs):
# 1. 用序列化器校验数据
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
# 2. 保存(调用 perform_create,可以重写)
self.perform_create(serializer)
# 3. 返回序列化后的数据 + 201 状态码
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=201, headers=headers)
def perform_create(self, serializer):
serializer.save() # 默认只保存,不填额外字段
为什么重写 perform_create:默认
perform_create只调serializer.save(),不会自动填created_by。重写后可以从request.user取当前用户自动填充。
2. ViewSet 实例
class InfoItemViewSet(viewsets.ModelViewSet):
queryset = InfoItem.objects.all() # 数据集
serializer_class = InfoItemSerializer # 序列化器
permission_classes = [IsOwnerOrReadOnly] # 权限(覆盖全局配置)
# 字段精确过滤:?category=3&tags=1&status=published
filterset_fields = ['category', 'tags', 'status']
# 模糊搜索:?search=关键词 → 同时搜 title 和 description
search_fields = ['title', 'description']
# 排序:?ordering=-created_at(倒序)
ordering_fields = ['created_at', 'priority', 'published_at']
def perform_create(self, serializer):
serializer.save(created_by=self.request.user, updated_by=self.request.user)
def perform_update(self, serializer):
serializer.save(updated_by=self.request.user)
| 属性 | 作用 |
|---|---|
queryset |
该视图集操作的数据集 |
serializer_class |
使用的序列化器类 |
permission_classes |
权限类(覆盖全局 DEFAULT_PERMISSION_CLASSES) |
filterset_fields |
精确过滤字段 |
search_fields |
模糊搜索字段 |
ordering_fields |
排序字段 |
pagination_class |
分页类(覆盖全局) |
3. perform_create / perform_update 钩子
# 默认实现(DRF 源码):
def perform_create(self, serializer):
serializer.save()
# 重写后:从 JWT 取当前用户,自动填 created_by / updated_by
def perform_create(self, serializer):
serializer.save(
created_by=self.request.user,
updated_by=self.request.user,
)
def perform_update(self, serializer):
serializer.save(updated_by=self.request.user)
serializer.save(**kwargs)中传的 kwargs 会覆盖序列化器中的同名字段。前端就算传了created_by也会被忽略。
4. @action 自定义端点
from rest_framework.decorators import action
from rest_framework.response import Response
class InfoItemViewSet(viewsets.ModelViewSet):
...
# detail=False → 作用于集合(URL 不带 ID)
@action(detail=False, methods=['get'])
def stats(self, request):
"""统计:GET /api/info-items/stats/"""
queryset = self.get_queryset()
total = queryset.count()
status_counts = queryset.values('status').annotate(count=Count('id'))
return Response({
'total': total,
'by_status': {item['status']: item['count'] for item in status_counts}
})
@action(detail=False, methods=['post'])
def batch_delete(self, request):
"""批量删除:POST /api/info-items/batch_delete/"""
ids = request.data.get('ids', [])
deleted, _ = InfoItem.objects.filter(id__in=ids).delete()
return Response({'deleted': deleted})
# detail=True → 作用于单条记录(URL 带 ID)
@action(detail=True, methods=['get'])
def attachments(self, request, pk=None):
"""获取附件:GET /api/info-items/{id}/attachments/"""
item = self.get_object() # 根据 URL 中的 pk 自动查出对象
attachments = item.attachments.all() # 通过 related_name 反查
serializer = AttachmentSerializer(attachments, many=True, context={'request': request})
return Response(serializer.data)
@action(detail=True, methods=['get'])
def comments(self, request, pk=None):
"""获取评论:GET /api/info-items/{id}/comments/"""
item = self.get_object()
comments = item.comments.filter(parent__isnull=True) # 只查一级评论
serializer = CommentSerializer(comments, many=True, context={'request': request})
return Response(serializer.data)
@action 参数
| 参数 | 作用 |
|---|---|
detail=True |
URL 带 pk,如 /items/{id}/attachments/ |
detail=False |
URL 不带 pk,如 /items/stats/ |
methods=['get'] |
允许的 HTTP 方法 |
url_path='xxx' |
自定义 URL 路径(默认用方法名) |
url_name='xxx' |
URL name(用于 reverse) |
URL 生成规则
detail=False → /api/{prefix}/{method_name}/
stats → /api/info-items/stats/
batch_delete → /api/info-items/batch_delete/
detail=True → /api/{prefix}/{pk}/{method_name}/
attachments → /api/info-items/1/attachments/
comments → /api/info-items/1/comments/
5. get_object / get_queryset
# get_object():根据 URL 中的 pk 自动查出单条记录
item = self.get_object() # 等价于 get_object_or_404(InfoItem, pk=pk)
# get_queryset():获取当前视图集的 queryset
qs = self.get_queryset() # 等价于 self.queryset
# get_serializer():获取序列化器实例
serializer = self.get_serializer(data=request.data)
十、DRF 路由 (Router)
1. DefaultRouter
from rest_framework.routers import DefaultRouter
from info_manage.views import InfoItemViewSet, CategoryViewSet, TagViewSet, \
AttachmentViewSet, CommentViewSet
router = DefaultRouter()
router.register('info-items', InfoItemViewSet, basename='info-item')
router.register('categories', CategoryViewSet, basename='category')
router.register('tags', TagViewSet, basename='tag')
router.register('attachments', AttachmentViewSet, basename='attachment')
router.register('comments', CommentViewSet, basename='comment')
urlpatterns = router.urls
register 参数
| 参数 | 作用 |
|---|---|
prefix |
URL 前缀,如 'info-items' → /api/info-items/ |
viewset |
视图集类 |
basename |
URL name 前缀,用于 reverse:reverse('info-item-list') |
2. URL 自动生成规则
注册 router.register('info-items', InfoItemViewSet, basename='info-item') 后:
| HTTP 方法 | URL | 对应方法 | URL name |
|---|---|---|---|
| GET | /api/info-items/ |
list() |
info-item-list |
| POST | /api/info-items/ |
create() |
info-item-list |
| GET | /api/info-items/{id}/ |
retrieve() |
info-item-detail |
| PUT | /api/info-items/{id}/ |
update() |
info-item-detail |
| PATCH | /api/info-items/{id}/ |
partial_update() |
info-item-detail |
| DELETE | /api/info-items/{id}/ |
destroy() |
info-item-detail |
| GET | /api/info-items/stats/ |
stats() (@action) |
info-item-stats |
| GET | /api/info-items/{id}/attachments/ |
attachments() (@action) |
info-item-attachments |
3. DefaultRouter vs SimpleRouter
| 特性 | DefaultRouter | SimpleRouter |
|---|---|---|
| 根路径 | 有 API 根视图(列出所有路由) | 无 |
| URL 尾斜杠 | 有(/items/) |
可选 |
| 适用场景 | 开发环境(带浏览界面) | 生产环境 |
项目级
urls.py中通过include()挂载:
path('api/', include('info_manage.api_urls')),
十一、认证与权限
1. 认证 vs 权限
请求进来 → 认证(Authentication) → 权限(Permission) → 视图
"你是谁?" "你能干什么?"
JWT / Session IsAuthenticated / IsOwnerOrReadOnly
失败 → 401 失败 → 403
| 概念 | 作用 | 失败状态码 |
|---|---|---|
| 认证 (Authentication) | 识别用户身份 | 401 Unauthorized |
| 权限 (Permission) | 控制访问权限 | 403 Forbidden |
2. JWT 认证 (SimpleJWT)
┌────────┐ POST /api/auth/token/ ┌──────────┐
│ 前端 │ ──── username+password ──────→ │ 后端 │
│ │ │ │
│ │ ←── access_token + refresh ──── │ │
│ │ └──────────┘
│ │
│ │ GET /api/info-items/ ┌──────────┐
│ │ ──── Authorization: Bearer xx → │ 后端 │
│ │ │ 验证JWT │
│ │ ←────── JSON 数据 ────────────── │ │
└────────┘ └──────────┘
access_token → 2 小时过期,每次 API 请求带它
refresh_token → 7 天过期,过期后用它换新的 access_token
# settings.py 中配置
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(hours=2),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
}
# urls.py 中配置 JWT 端点
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns = [
path('api/auth/token/', TokenObtainPairView.as_view()), # 获取 token
path('api/auth/token/refresh/', TokenRefreshView.as_view()), # 刷新 token
]
请求时怎么带 JWT:在 HTTP 头加
Authorization: Bearer <access_token>(注意 Bearer 后面有空格)。
3. Session 认证
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication', # 优先 JWT
'rest_framework.authentication.SessionAuthentication', # 备选 Session
],
Session 认证靠浏览器 Cookie 中的
sessionid。MVT 模板渲染时用这个。前后端分离时用 JWT。
4. 内置权限类
| 权限类 | 作用 |
|---|---|
IsAuthenticated |
必须登录 |
IsAdminUser |
必须是管理员(is_staff=True) |
AllowAny |
任何人都可以 |
IsAuthenticatedOrReadOnly |
读操作任何人可访问,写操作需登录 |
# 全局配置(settings.py 中所有 API 生效)
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
# 局部覆盖(单个 ViewSet 中设置)
class InfoItemViewSet(viewsets.ModelViewSet):
permission_classes = [IsOwnerOrReadOnly] # 覆盖全局配置
5. 自定义权限 (IsOwnerOrReadOnly)
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""只有创建人能修改/删除,其他人只能读"""
# 视图级权限:每个请求都先过这里
def has_permission(self, request, view):
return request.user.is_authenticated # 未登录直接拒绝
# 对象级权限:操作单条记录时(retrieve/update/destroy)才过这里
def has_object_permission(self, request, view, obj):
# 读操作(GET/HEAD/OPTIONS)所有人都能访问
if request.method in permissions.SAFE_METHODS:
return True
# 写操作(POST/PUT/PATCH/DELETE)只有创建人可以
return obj.created_by == request.user
两级权限执行流程
请求 → has_permission() → True? → 继续
False? → 403
↓ (如果是操作单条记录)
has_object_permission() → True? → 允许操作
False? → 403
| 方法 | 调用时机 | 典型用途 |
|---|---|---|
has_permission |
每个请求都调用 | 检查是否登录 |
has_object_permission |
retrieve/update/destroy 时调用 | 检查是否是记录的拥有者 |
⚠️
has_object_permission只在get_object()被调用时触发,也就是操作单条记录时。list()和create()不会触发。
SAFE_METHODS
permissions.SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
这些是"安全方法"——不会修改数据,所以权限可以放宽。
十二、过滤 / 搜索 / 排序 / 分页
1. DjangoFilterBackend(字段精确过滤)
class InfoItemViewSet(viewsets.ModelViewSet):
filterset_fields = ['category', 'tags', 'status']
GET /api/info-items/?category=3&status=published
→ WHERE category_id=3 AND status='published'
精确匹配,不支持模糊搜索。适合状态、分类等枚举字段。
2. SearchFilter(模糊搜索)
class InfoItemViewSet(viewsets.ModelViewSet):
search_fields = ['title', 'description']
GET /api/info-items/?search=Python
→ WHERE title LIKE '%Python%' OR description LIKE '%Python%'
模糊搜索,不区分大小写。用
?search=关键词触发。
搜索模式
# 默认:包含搜索(icontains)
search_fields = ['title', 'description']
# 前缀搜索(istartswith)
search_fields = ['=title'] # 以关键词开头
# 完全匹配
search_fields = ['^title'] # 正则匹配开头
# 正则表达式
search_fields = ['$title'] # 正则匹配
3. OrderingFilter(排序)
class InfoItemViewSet(viewsets.ModelViewSet):
ordering_fields = ['created_at', 'priority', 'published_at']
GET /api/info-items/?ordering=-created_at → ORDER BY created_at DESC(倒序)
GET /api/info-items/?ordering=priority → ORDER BY priority ASC(正序)
GET /api/info-items/?ordering=-priority,title → ORDER BY priority DESC, title ASC
-前缀表示倒序,不加表示正序。可以多字段排序(逗号分隔)。
4. PageNumberPagination(分页)
# settings.py 全局配置
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
}
GET /api/info-items/?page=2 → 第2页
GET /api/info-items/?page=2&page_size=20 → 第2页,每页20条
分页返回格式
{
"count": 42, // 总记录数
"next": "http://...?page=3", // 下一页 URL(null 表示没有)
"previous": "http://...?page=1", // 上一页 URL
"results": [...] // 当前页数据
}
自定义分页类
from rest_framework.pagination import PageNumberPagination
class CustomPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size' # 允许前端指定每页条数
max_page_size = 100 # 最大每页条数
class InfoItemViewSet(viewsets.ModelViewSet):
pagination_class = CustomPagination # 局部覆盖
| 分页类 | URL 参数 | 特点 |
|---|---|---|
| PageNumberPagination | ?page=2 |
按页码 |
| LimitOffsetPagination | ?limit=10&offset=20 |
按偏移量 |
| CursorPagination | ?cursor=xxx |
加密游标(不可跳页,适合大数据) |
十三、异常处理
1. DRF 默认异常
| 异常 | HTTP 状态码 | 触发场景 |
|---|---|---|
NotAuthenticated |
401 | 未登录 |
PermissionDenied |
403 | 无权限 |
NotFound |
404 | 记录不存在 |
ValidationError |
400 | 数据校验失败 |
MethodNotAllowed |
405 | 方法不允许 |
Throttled |
429 | 限流 |
DRF 默认返回格式:
{"detail": "错误信息"},前后端格式不统一。
2. 自定义 exception_handler
import logging
from rest_framework.views import exception_handler
from rest_framework.response import Response
logger = logging.getLogger(__name__)
def custom_exception_handler(exc, context):
# 第一步:先交给 DRF 默认处理器
response = exception_handler(exc, context)
if response is not None:
# DRF 认识的异常 → 统一格式化
detail = response.data.get('detail', str(response.data))
response.data = {
'code': response.status_code,
'message': str(detail),
'data': None,
}
return response
# DRF 不认识的异常(如 IntegrityError、KeyError)→ 记日志 + 返回 500
logger.exception("未捕获异常")
return Response(
{'code': 500, 'message': '服务器内部错误', 'data': None},
status=500
)
# settings.py 中注册
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'info_manage.exceptions.custom_exception_handler',
}
统一返回格式
// 成功
{"code": 200, "message": "success", "data": {...}}
// 失败
{"code": 401, "message": "Given token not valid for any token type", "data": null}
{"code": 404, "message": "Not found.", "data": null}
为什么要统一格式:前端只需要判断
code字段就知道是否成功,不需要看 HTTP 状态码。
十四、文件上传
1. MEDIA 配置
# settings.py
MEDIA_ROOT = os.path.join(BASE_DIR, "media") # 文件存储根目录
MEDIA_URL = "/media/" # URL 前缀
# urls.py — 挂载媒体文件路由
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
...
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
⚠️
static()返回的是列表[URLPattern(...)],必须用+拼接到urlpatterns后面,不能作为列表项直接放进去。⚠️
static()只在DEBUG=True时生效。生产环境用 Nginx 直接服务媒体文件。
2. FileField / ImageField
class Attachment(BaseModel):
file = models.FileField(upload_to="attachments/", verbose_name="文件")
# ImageField 会校验是否为图片
# avatar = models.ImageField(upload_to="avatars/")
| 参数 | 作用 |
|---|---|
upload_to |
文件保存的子目录(相对于 MEDIA_ROOT) |
max_length |
文件路径最大长度(默认 100) |
上传的文件会保存到
MEDIA_ROOT/attachments/xxx.pdf,数据库中存的是相对路径attachments/xxx.pdf。
upload_to 高级用法
# 按日期分目录
file = models.FileField(upload_to="attachments/%Y/%m/%d/")
# 自定义函数
def user_directory_path(instance, filename):
return f'attachments/{instance.uploaded_by.id}/{filename}'
file = models.FileField(upload_to=user_directory_path)
3. ViewSet 上传处理
class AttachmentViewSet(viewsets.ModelViewSet):
queryset = Attachment.objects.all()
serializer_class = AttachmentSerializer
def perform_create(self, serializer):
"""上传时自动提取文件信息"""
uploaded_file = serializer.validated_data['file']
serializer.save(
uploaded_by=self.request.user,
original_name=uploaded_file.name, # 原始文件名
file_size=uploaded_file.size, # 文件大小(字节)
)
serializer.validated_data是经过校验的数据字典。文件字段返回的是 Django 的UploadedFile对象,有.name、.size属性。
4. 前端上传 (FormData)
// 前端必须用 multipart/form-data,不能用 JSON
const formData = new FormData()
formData.append('file', file) // 文件本体
formData.append('info_item', itemId) // 关联记录 ID
// 注意:不要手动设 Content-Type!
// 浏览器传 FormData 时会自动加 boundary
request.post('/attachments/', formData)
⚠️ 如果手动设
Content-Type: multipart/form-data,会丢掉 boundary 参数,后端无法解析文件。
十五、自引用与嵌套序列化
1. 自引用外键 (“self”)
class Comment(BaseModel):
info_item = models.ForeignKey("InfoItem", on_delete=models.CASCADE, related_name="comments")
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="comments")
content = models.TextField()
parent = models.ForeignKey(
"self", # "self" 表示指向自己这张表
on_delete=models.CASCADE,
null=True, # 一级评论没有父评论
blank=True,
related_name="replies", # 父评论.replies.all() 查所有回复
)
Comment 表结构:
┌────┬───────────┬────────┬──────────────────┐
│ id │ content │ author │ parent_id (FK→self) │
├────┼───────────┼────────┼──────────────────┤
│ 1 │ 一级评论A │ 用户1 │ NULL │ ← 一级评论
│ 2 │ 一级评论B │ 用户2 │ NULL │ ← 一级评论
│ 3 │ 回复A-1 │ 用户2 │ 1 │ ← 回复评论1
│ 4 │ 回复A-2 │ 用户3 │ 1 │ ← 回复评论1
│ 5 │ 回复B-1 │ 用户1 │ 2 │ ← 回复评论2
└────┴───────────┴────────┴──────────────────┘
查询树形结构:
一级评论A (id=1)
├── 回复A-1 (id=3) ← parent_id=1
└── 回复A-2 (id=4) ← parent_id=1
一级评论B (id=2)
└── 回复B-1 (id=5) ← parent_id=2
"self"是 Django 特殊字符串,表示外键指向同一张表。parent_id存的是同表的另一行的 id。
2. 评论树形结构查询
# 只查一级评论(parent 为 NULL)
comments = item.comments.filter(parent__isnull=True)
# parent__isnull=True 是 Django ORM 双下划线语法
# 等价于 SQL: WHERE parent_id IS NULL
3. 限深嵌套实现
class CommentSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source='author.username', read_only=True)
replies = serializers.SerializerMethodField()
class Meta:
model = Comment
fields = ['id', 'info_item', 'author', 'author_name', 'content',
'parent', 'replies', 'created_at']
read_only_fields = ['author']
def get_replies(self, obj):
if obj.parent is not None:
return [] # 当前是回复 → 不再递归
replies = obj.replies.all()
return CommentSerializer(replies, many=True, context=self.context).data
限深关键:
if obj.parent is not None: return []。如果当前评论本身是一条回复(有 parent),就返回空列表,不再嵌套。这样递归只走一层就停。
为什么不用 depth=1:DRF 内置的
depth会展开所有外键(info_item、author、parent 全展开),而且嵌套对象变只读。用SerializerMethodField可以精确控制只嵌套replies。
十六、级联删除专题
1. .delete() 返回值
deleted, details = InfoItem.objects.filter(id__in=[1, 2, 3]).delete()
# deleted = 10
# details = {'info_manage.InfoItem': 3, 'info_manage.InfoItem_tags': 7}
.delete()返回元组(总数, {表名: 删除数})。总数包含级联删除的所有表记录。
2. 级联删除范围
删除 InfoItem (id=1) 时会级联删除:
┌─────────────────────────────────┐
│ InfoItem (id=1) │ ← 手动删除
│ on_delete=CASCADE │
├─────────────────────────────────┤
│ ├── Attachment (info_item=1) │ ← CASCADE 级联删除
│ ├── Comment (info_item=1) │ ← CASCADE 级联删除
│ │ └── Comment (parent=上面) │ ← CASCADE 级联删除回复
│ └── InfoItem_tags 中间表记录 │ ← M2M 中间表自动删除
├─────────────────────────────────┤
│ Category ← PROTECT 不删 │ ← PROTECT 保护
│ User ← PROTECT 不删 │ ← PROTECT 保护
└─────────────────────────────────┘
| on_delete 选项 | 是否级联 | 说明 |
|---|---|---|
CASCADE |
✅ 是 | 父记录删 → 子记录也删 |
PROTECT |
❌ 否 | 有子记录时禁止删父记录 |
SET_NULL |
❌ 否 | 父记录删 → 子记录外键设 NULL |
SET_DEFAULT |
❌ 否 | 父记录删 → 子记录外键设默认值 |
M2M 中间表:多对多关系生成的中间表记录,在删除任一方时都会被自动删除(不受 on_delete 控制)。
3. 批量删除实例
@action(detail=False, methods=['post'])
def batch_delete(self, request):
"""批量删除:POST /api/info-items/batch_delete/ {"ids": [1,2,3]}"""
ids = request.data.get('ids', [])
deleted, details = InfoItem.objects.filter(id__in=ids).delete()
return Response({'deleted': deleted})
传
{"ids": [1, 2, 3]}返回{"deleted": 10}而非 3,因为.delete()返回的是级联删除总数(3 条记录 + 7 条中间表记录)。用
deleted, details = ...解构可以看到明细:{'info_manage.InfoItem': 3, 'info_manage.InfoItem_tags': 7}。
十七、前后端分离架构
1. JWT 认证完整流程
┌─────────┐ ┌─────────┐
│ 前端 │ │ 后端 │
│ (Vue) │ │(Django) │
└────┬────┘ └────┬────┘
│ │
│ 1. POST /api/auth/token/ {username,password} │
│ ──────────────────────────────────────────→ │
│ │
│ 2. 返回 {access: "xxx", refresh: "yyy"} │
│ ←────────────────────────────────────────── │
│ │
│ 3. localStorage.setItem('token', access) │
│ │
│ 4. GET /api/info-items/ │
│ Authorization: Bearer xxx │
│ ──────────────────────────────────────────→ │
│ │
│ 5. 返回 JSON 数据 │
│ ←────────────────────────────────────────── │
│ │
│ ··· 2 小时后 access 过期 ··· │
│ │
│ 6. POST /api/auth/token/refresh/ │
│ {refresh: "yyy"} │
│ ──────────────────────────────────────────→ │
│ │
│ 7. 返回新的 {access: "zzz"} │
│ ←────────────────────────────────────────── │
└──────────────────────────────────────────────┘
2. axios 拦截器(前端 JWT 自动携带)
// 请求拦截器:每个请求自动带 token
request.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}` // 自动加 JWT
}
return config
})
// 响应拦截器:token 过期自动跳登录
request.interceptors.response.use(
(response) => response.data, // 直接返回 data,不用 .data
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('token')
router.push('/login') // 跳转登录页
}
return Promise.reject(error)
}
)
3. CORS 配置(前后端不同端口时)
# settings.py
INSTALLED_APPS = [
...
'corsheaders', # 添加 CORS 应用
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # 必须放在最前面!
...
]
# 允许所有来源(开发环境)
CORS_ALLOW_ALL_ORIGINS = True
# 或指定来源(生产环境)
# CORS_ALLOWED_ORIGINS = [
# 'http://localhost:5173',
# 'http://127.0.0.1:5173',
# ]
⚠️
CorsMiddleware必须放在CommonMiddleware前面,否则 CORS 头不会正确添加。
4. Vite 代理(开发环境免 CORS)
// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8000', // Django 后端
changeOrigin: true,
},
'/media': {
target: 'http://localhost:8000', // 媒体文件
changeOrigin: true,
},
}
}
})
前端请求
/api/items/时,Vite 自动转发到http://localhost:8000/api/items/。浏览器看到的还是同源请求,不会有 CORS 问题。生产环境用 Nginx 代理。
十八、自定义用户模型
1. 为什么自定义用户模型
Django 内置 User 模型有 username/password/email/first_name/last_name 等字段。如果需要额外字段(如手机号、部门),有两种方式:
| 方式 | 做法 | 优缺点 |
|---|---|---|
| OneToOne 扩展 | 建 UserProfile 表关联 User | 不破坏原有 User,但不能改 username |
| 继承 AbstractUser | 自定义 User 模型 | 灵活,推荐但必须在 migrate 前设置 |
2. 继承 AbstractUser
# models/User.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
"""自定义用户——继承 AbstractUser,自带所有认证功能"""
# 可以在这里加字段,如 phone = models.CharField(...)
# 本项目用 UserProfile 一对一扩展,所以 User 不加字段
class Meta:
verbose_name = "用户"
verbose_name_plural = verbose_name
def __str__(self):
return self.username
# settings.py(必须在第一次 migrate 前设置!)
AUTH_USER_MODEL = "info_manage.User"
⚠️
AUTH_USER_MODEL必须在第一次 migrate 之前设置。如果已经 migrate 过,需要删库重建。
3. UserProfile 一对一扩展
# models/UserProfile.py
class UserProfile(BaseModel):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="profile", # user.profile.phone 反查
)
phone = models.CharField(max_length=20, blank=True, default="")
department = models.CharField(max_length=100, blank=True, default="")
related_name="profile"后,user.profile就能取到扩展信息。如果用户没创建 UserProfile,访问user.profile会报RelatedObjectDoesNotExist。
十九、settings 多环境分离
1. 为什么要分离
开发 / 测试 / 生产环境的配置不同(数据库、密钥、DEBUG),把密码写死在 settings.py 里既不安全也不灵活。拆成"公共 + 多环境"结构,敏感信息放 .env(不提交 Git)。
2. 目录结构(包代替单文件)
djangoproject/
├── settings/
│ ├── __init__.py # 根据环境变量决定加载哪个环境
│ ├── base.py # 公共配置(INSTALLED_APPS / MIDDLEWARE / REST_FRAMEWORK 等)
│ ├── development.py # 开发环境(DEBUG=True)
│ └── production.py # 生产环境(DEBUG=False,正式库)
└── ...
3. base.py 用 dotenv 加载敏感信息
from dotenv import load_dotenv
import os
load_dotenv() # 加载项目根目录的 .env 文件
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'dev-insecure-key')
DEBUG = os.getenv('DJANGO_DEBUG', 'True') == 'True' # 字符串转布尔
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME', 'test'),
'USER': os.getenv('DB_USER', 'admin'),
'PASSWORD': os.getenv('DB_PASSWORD', '123'),
'HOST': os.getenv('DB_HOST', 'localhost'),
'PORT': os.getenv('DB_PORT', '5432'),
}
}
.env(不要提交 Git):
DJANGO_SECRET_KEY=你的密钥
DJANGO_DEBUG=True
DB_NAME=test
DB_USER=admin
DB_PASSWORD=123
4. init.py 按环境变量切换
import os
env = os.getenv('DJANGO_ENV', 'development')
if env == 'production':
from .production import *
else:
from .development import *
5. 各环境文件继承 base
# development.py
from .base import * # 先继承全部公共配置
DEBUG = True
# 开发环境可覆盖:用 SQLite、放开 CORS 等
# production.py
from .base import *
DEBUG = False
ALLOWED_HOSTS = ['www.example.com']
# 生产环境覆盖:正式数据库、关掉调试、配正式 CORS
启动 / 迁移时指定环境:
DJANGO_SETTINGS_MODULE='djangoproject.settings.development'(PyCharm 的运行配置里改这个变量)。
二十、drf-yasg 自动 API 文档
1. 安装与配置
pip install drf-yasg
总 urls.py 里注册 schema 视图:
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from rest_framework import permissions
schema_view = get_schema_view(
openapi.Info(
title="信息管理 API",
default_version='v1',
description="DRF 学习项目接口文档",
),
public=True,
permission_classes=[permissions.AllowAny], # 文档页允许任何人访问
)
urlpatterns = [
# ... 其他路由 ...
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
]
访问 /swagger/ 是交互式文档(可直接填参数试调),/redoc/ 是静态排版文档。DRF 会根据 ViewSet 自动生成所有端点。
2. 自定义参数(文件上传必看)
drf-yasg 默认把 POST 当 application/json,不会显示文件上传框。上传附件必须显式声明:
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from rest_framework.decorators import action
from rest_framework.parsers import MultiPartParser, FormParser
@swagger_auto_schema(
method='post',
operation_description='上传附件',
manual_parameters=[
openapi.Parameter(
'file', openapi.IN_FORM, # 关键:声明为表单字段
description='附件文件',
type=openapi.TYPE_FILE,
required=True,
),
],
)
@action(detail=True, methods=['post'], parser_classes=[MultiPartParser, FormParser])
def attachments(self, request, pk=None):
...
三件套缺一不可:
manual_parameters让 swagger 显示 file 控件 +parser_classes让后端能解析multipart/form-data。
二十一、GenericForeignKey 通用外键
1. 解决什么问题
普通 ForeignKey 只能指向固定一张表。但有时一条记录想关联"任意模型"——比如附件可以挂到信息记录、评论等任意对象上。通用外键(GenericForeignKey)用 ContentType 表做中转,实现"一条记录关联任意模型"。
2. 三件套
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
class Attachment(BaseModel):
# 1. content_type:指向哪个模型(外键到 django_content_type 表)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
# 2. object_id:那条记录的主键 id
object_id = models.PositiveIntegerField()
# 3. content_object:ORM 语法糖,直接拿关联对象(不存数据库)
content_object = GenericForeignKey('content_type', 'object_id')
file = models.FileField(upload_to='attachments/')
| 字段 | 是否存数据库 | 作用 |
|---|---|---|
content_type |
✅ | 指向 ContentType 表的哪一行(即哪个模型) |
object_id |
✅ | 目标记录的主键 |
content_object |
❌ | 语法糖,运行时通过上面两字段定位对象 |
3. 反向查询(在被关联模型上加)
class InfoItem(BaseModel):
# GenericRelation 让 item.attachments.all() 能反查出所有挂自己的附件
attachments = GenericRelation('Attachment')
4. 视图中推断并注入
前端只传 file,content_type 从 URL 推断,后端注入:
item = self.get_object()
content_type = ContentType.objects.get_for_model(item) # 拿到 InfoItem 的 ContentType
serializer.save(
content_type=content_type,
object_id=item.id,
uploaded_by=request.user,
original_name=uploaded_file.name,
file_size=uploaded_file.size,
)
5. 序列化器注意事项(易踩坑)
class AttachmentSerializer(serializers.ModelSerializer):
file_url = serializers.SerializerMethodField()
class Meta:
model = Attachment
fields = ['id', 'content_type', 'object_id', 'file', 'file_url',
'original_name', 'file_size', 'uploaded_by', 'created_at']
extra_kwargs = {
# content_type / object_id 由 save() 注入,不来自前端
# 不设 required=False,is_valid() 会在 save() 之前就因"必填项"而 400
'content_type': {'required': False},
'object_id': {'required': False},
'original_name': {'required': False},
'file_size': {'required': False},
}
def get_file_url(self, obj):
request = self.context.get('request')
if obj.file and request:
return request.build_absolute_uri(obj.file.url)
return None
调试技巧:用
f"{obj.content_type.app_label}.{obj.content_type.model}"在序列化器里显示关联模型名(如info_manage.infoitem)。
6. N+1 风险
GenericForeignKey **不能用 **select_related(它不是真正的 FK 列)。查询关联对象时有 N+1 风险,必要时用 prefetch_related 或接受。
二十二、RBAC 角色权限(企业级重点)
1. 概念:和 IsOwnerOrReadOnly 的区别
之前的 IsOwnerOrReadOnly 只判断"是不是创建人"——一个维度。RBAC(基于角色的访问控制)升级成"用户 → 角色 → 权限"三层,权限维度更灵活:同一用户可以有多个角色,每个角色继承父角色的权限。
IsOwnerOrReadOnly: 请求 → 是创建人? → 放行/拒绝
RBAC: 请求 → 用户有哪些角色? → 角色有哪些权限(含继承)? → 有该权限?
2. 数据模型
class Role(BaseModel):
name = models.CharField(max_length=50) # 显示名,如"管理员"
code = models.CharField(max_length=50, unique=True) # 程序判断用,如"admin"
parent = models.ForeignKey( # 自引用继承(和评论 parent 同套路)
'self', null=True, blank=True,
on_delete=models.CASCADE, related_name='children')
permissions = models.TextField(blank=True) # 权限码,逗号分隔字符串
class UserRole(BaseModel):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='user_roles')
role = models.ForeignKey('Role', on_delete=models.CASCADE, related_name='role_users')
角色继承链:
admin(删) → editor(增改) → viewer(查)。admin 继承 editor 的"增改" + viewer 的"查" = 全部四项权限。箭头方向是子 → 父。
3. 权限双层架构
RBACPermission(DRF 权限类) RolePermissionBackend(Django 认证后端)
"决定查什么权限码" "决定怎么查权限"
│ │
└──── request.user.has_perm(perm) ──────┘
│
遍历 AUTHENTICATION_BACKENDS 链
3.1 RBACPermission(DRF 层,决定查什么)
class RBACPermission(permissions.BasePermission):
def has_permission(self, request, view):
action_perms = getattr(view, 'action_permissions', None)
if not action_perms:
return True # 视图没配权限映射 → 放行
required_perm = action_perms.get(view.action) # 按当前动作查权限码
if not required_perm:
return True
return request.user.has_perm(required_perm)
3.2 RolePermissionBackend(Django 认证后端,决定怎么查)
class RolePermissionBackend:
def has_perm(self, user_obj, perm, obj=None):
from info_manage.models import UserRole # 延迟导入防循环
user_roles = UserRole.objects.filter(user=user_obj).select_related('role')
for ur in user_roles:
if self._role_has_perm(ur.role, perm):
return True
return False
def _role_has_perm(self, role, perm, _checked=None):
if _checked is None:
_checked = set()
if role.id in _checked: # 防循环继承死递归(A→B→A)
return False
_checked.add(role.id)
if perm in [p.strip() for p in role.permissions.split(',')]:
return True
if role.parent:
return self._role_has_perm(role.parent, perm, _checked)
return False
def get_all_permissions(self, user_obj):
"""收集用户全部权限码(含继承),返回 set,供前端 UserInfoView 用"""
from info_manage.models import UserRole
perms = set()
for ur in UserRole.objects.filter(user=user_obj).select_related('role'):
self._collect_role_perms(ur.role, perms)
return perms
def _collect_role_perms(self, role, perms, _checked=None):
"""递归收集一个角色 + 其所有父角色链上的权限码(set 去重 + 防循环继承)"""
if _checked is None:
_checked = set()
if role.id in _checked: # 已查过 → 直接返回,避免 A→B→A 死递归
return
_checked.add(role.id)
for p in role.permissions.split(','):
p = p.strip()
if p:
perms.add(p) # set 自动去重
if role.parent: # 递归向上收集父角色(继承)
self._collect_role_perms(role.parent, perms, _checked)
select_related('role')防 N+1;_checked(set 记录已查角色 id)防循环继承(角色环 A→B→A);split(',')后必须strip()去空格。
4. ViewSet 声明(按 action 映射权限码)
class InfoItemViewSet(viewsets.ModelViewSet):
permission_classes = [IsOwnerOrReadOnly, RBACPermission] # 两个取交集
action_permissions = {
'list': 'info_manage.view_infoitem',
'retrieve': 'info_manage.view_infoitem',
'create': 'info_manage.add_infoitem',
'update': 'info_manage.change_infoitem',
'partial_update': 'info_manage.change_infoitem',
'destroy': 'info_manage.delete_infoitem',
}
权限码格式(Django 约定):app_label.动作_模型名小写
| 动作 | 权限码 |
|---|---|
| 查看 | info_manage.view_infoitem |
| 新增 | info_manage.add_infoitem |
| 修改 | info_manage.change_infoitem |
| 删除 | info_manage.delete_infoitem |
5. 注册双后端(settings)
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend', # Django 默认(查 user_permissions 表)
'info_manage.backends.RolePermissionBackend', # 自定义 RBAC(查角色权限)
]
两个后端并存,
has_perm()遍历链,任一返回 True 即放行。老代码用user_permissions的照常工作,新代码用角色的也工作。
6. 前端联调(按权限控制按钮)
后端 + 前端配合,让按钮跟着用户角色显隐:
登录 → 拿 token → 调 GET /api/auth/user/ → 返回 {username, roles, permissions}
→ 前端 store 持久化 → 列表页 v-if="hasPerm('info_manage.add_infoitem')" 控制按钮
- 后端加
UserInfoView(GET /api/auth/user/),复用get_all_permissions()收集权限码 - 前端登录后调该接口,把权限码存到 user store(reactive + localStorage,项目未装 Pinia)
- 列表页
v-if="hasPerm('info_manage.add_infoitem')"控制"新增"按钮显隐
7. 测试账号(密码统一 123456)
| 账号 | 角色 | 权限 |
|---|---|---|
test_admin |
管理员 admin | 全部(view + add + change + delete) |
test_editor |
编辑 editor | view + add + change(无 delete) |
test_viewer |
访客 viewer | 仅 view |
用不同账号登录前端,能看到"新增 / 编辑 / 删除"按钮按角色显隐,与后端 RBAC 一致。
附录:常用查询速查表
ORM 查询
# ──── 基础查询 ────
Model.objects.all() # 全部
Model.objects.get(pk=1) # 查单条(不存在报 DoesNotExist)
Model.objects.filter(field=value) # 过滤
Model.objects.exclude(field=value) # 排除
Model.objects.order_by('-field') # 排序
Model.objects.first() / .last() # 第一/最后一条
Model.objects.count() # 计数
Model.objects.exists() # 是否存在
Model.objects.none() # 空 QuerySet
# ──── 字段查找(双下划线)────
filter(field__exact=value) # 精确等于(默认)
filter(field__icontains='text') # 模糊搜索(不区分大小写)
filter(field__contains='text') # 包含(区分大小写)
filter(field__startswith='text') # 以...开头
filter(field__endswith='text') # 以...结尾
filter(field__in=[1, 2, 3]) # 在列表中
filter(field__gt=value) # 大于
filter(field__gte=value) # 大于等于
filter(field__lt=value) # 小于
filter(field__lte=value) # 小于等于
filter(field__range=(1, 10)) # 范围(含两端)
filter(field__isnull=True) # 为 NULL
filter(field__year=2025) # 按年
filter(field__month=7) # 按月
filter(field__date='2025-07-22') # 按日期
# ──── 关联查询 ────
item.category # 正向:取关联对象
item.tags.all() # 正向 M2M
category.items.all() # 反向 FK(用 related_name)
item.attachments.all() # 反向 FK
user.profile # 正向 O2O
# ──── 性能优化 ────
select_related('fk_field') # FK/OneToOne 用 JOIN(1 次查询)
prefetch_related('m2m_field') # M2M/FK反向 单独查(2 次查询)
# ──── 聚合 ────
from django.db.models import Count, Sum, Avg, Max, Min
Model.objects.aggregate(total=Count('id'), avg_price=Avg('price'))
Model.objects.values('category').annotate(count=Count('id')) # GROUP BY
# ──── F / Q 对象 ────
from django.db.models import F, Q
Model.objects.filter(views__gt=F('likes')) # 字段间比较
Model.objects.filter(Q(title__icontains='a') | Q(status='published')) # OR
# ──── 创建/更新/删除 ────
Model.objects.create(field=value) # 创建并保存
obj = Model(field=value); obj.save() # 创建后手动保存
obj.field = new_value; obj.save() # 修改
Model.objects.filter(pk=1).update(field=val) # 批量更新
obj.delete() # 删除单条
Model.objects.filter(id__in=ids).delete() # 批量删除
DRF 视图集方法速查
# ──── ModelViewSet 自带方法 ────
self.get_queryset() # 获取 queryset
self.get_object() # 根据 pk 查单条记录
self.get_serializer() # 获取序列化器实例
self.filter_queryset(qs) # 应用过滤
self.paginate_queryset(qs) # 应用分页
self.get_paginated_response(data) # 生成分页响应
# ──── 可重写的钩子方法 ────
perform_create(serializer) # POST 时调用
perform_update(serializer) # PUT/PATCH 时调用
perform_destroy(instance) # DELETE 时调用
get_queryset() # 动态返回 queryset
get_serializer_class() # 动态返回序列化器
get_permissions() # 动态返回权限列表
# ──── request 对象 ────
request.data # POST/PUT 的 JSON 数据
request.query_params # GET 参数(等价于 request.GET)
request.user # 当前用户
request.method # HTTP 方法
request.FILES # 上传的文件
HTTP 状态码速查
| 状态码 | 含义 | DRF 场景 |
|---|---|---|
| 200 | OK | GET 成功 |
| 201 | Created | POST 创建成功 |
| 204 | No Content | DELETE 成功 |
| 400 | Bad Request | 数据校验失败 |
| 401 | Unauthorized | 未认证 / token 无效 |
| 403 | Forbidden | 无权限 |
| 404 | Not Found | 记录不存在 |
| 405 | Method Not Allowed | 方法不允许 |
| 429 | Too Many Requests | 限流 |
| 500 | Internal Server Error | 服务器错误 |