Django
前言
草木何曾争雨露,江山自古属闲人
1.Django初始
1.1介绍
Django 是 Python 最流行的 Web 框架之一,属于 全功能框架(Full-stack framework)。由荷兰人Armin Ronacher创建。
核心目标:
- 快速开发 Web 应用
- 减少重复代码
- 安全性高
- 可扩展性好
官网口号:The web framework for perfectionists with deadlines.
Django官网
https://www.djangoproject.com/

Django 采用 MTV 架构模式
| Django | 类似 MVC |
|---|---|
| Model | Model |
| Template | View |
| View | Controller |
整体流程:
浏览器请求
↓
URL 路由 (urls.py)
↓
View 视图函数 (views.py)
↓
Model 数据库操作 (models.py)
↓
Template 模板渲染
↓
返回 HTML 页面
1.2版本
1.2.1Django1.x
特点:
- 默认 不支持异步
- 只支持 同步请求处理
请求流程:
请求 → Django → 处理 → 返回
如果需要异步任务,需要使用第三方工具:
常见方案:
- Celery(后台任务)
- Tornado(异步框架)
1.2.2Django2.x
特点:
- 仍然 不支持真正的异步视图
- 开始支持 ASGI 标准
ASGI 是:Asynchronous Server Gateway Interface
作用:让 Django 具备异步架构基础能力
如果要实现异步功能,需要搭配:
- ASGI 服务器
- daphne
- uvicorn
- channels
主要用于:
- WebSocket
- 实时通信
1.2.3Django3.x
重要变化:
开始 正式支持异步编程
新增能力:
- 支持
async view - 支持
asyncio - 支持 ASGI
- 支持 WebSocket
示例:
async def index(request):
return HttpResponse("hello")
Django 开始进入:
同步 + 异步 混合模式
1.2.4Django4.x
特点:
- 异步能力进一步增强
- 性能更好
- ASGI 支持更稳定
异步支持范围扩大:
- 视图
- 中间件
- 请求处理
适合:
- 高并发
- WebSocket
- 实时系统
1.2.5Django5.x
特点:
- 异步能力成熟且稳定
- 性能优化,更适合高并发
- ASGI 支持完善,可直接处理 HTTP/2 和 WebSocket
异步支持范围:
- 视图(async view)
- 中间件(async middleware)
- 请求处理及部分 ORM 查询
适合场景:
- 高并发 Web 应用
- WebSocket 通信
- 实时系统
- 企业级前后端分离项目
1.2.5总结
Django 3.x 之后才真正支持异步编程
Django 5.x(尤其 5.2 LTS)异步功能成熟、性能优化,是学习和企业项目的推荐版本。
| 版本 | 异步支持 |
|---|---|
| Django 1.x | 不支持 |
| Django 2.x | 开始支持 ASGI |
| Django 3.x | 正式支持 async |
| Django 4.x | 异步能力全面增强 |
| Django 5.x | 异步功能成熟稳定,性能优化,ASGI 支持完善,可处理 WebSocket 和高并发 |
1.3conda安装Django独立环境
# 1️⃣ 创建一个新的 Conda 环境(这里命名为 django_env,Python 3.12)
conda create -n django_env python=3.12
# 2️⃣ 查看已创建的环境
conda info --envs
# 3️⃣ 激活环境
conda activate django_env
# 4️⃣ 安装 Django 5.2 LTS
pip install Django==5.2
# 5️⃣ 验证 Django 是否安装成功
django-admin --version
# 6️⃣ 可选:安装 Django 常用扩展(这里暂不安装)
pip install djangorestframework channels psycopg2-binary
# 7️⃣ 查看当前环境已安装的包
pip list # pip 安装的包
conda list # conda 安装的包
# 8️⃣ 退出当前环境
conda deactivate
# 9️⃣ 删除环境(如果需要重新创建)
conda remove -n django_env --all
创建 django_env 环境
# 查看 python 环境
python --version
# 创建 django_env 环境
conda create -n django_env python=3.12

查看当前所有环境
conda info --envs

下面的安装过程放在一起了
# 激活环境
conda activate django_env
# 安装 Django 5.2 LTS
pip install Django==5.2
# 验证 Django 是否安装成功
django-admin --version
# 查看当前环境已安装的包
pip list # pip 安装的包
conda list # conda 安装的包

1.4Django命令行管理工具
安装之后会有django-admin.exe

1.5查看Django命令
django-admin
[django]
check
compilemessages
createcachetable
dbshell
diffsettings
dumpdata
flush
inspectdb
loaddata
makemessages
makemigrations
migrate
optimizemigration
runserver
sendtestemail
shell
showmigrations
sqlflush
sqlmigrate
sqlsequencereset
squashmigrations
startapp
startproject
test
testserver

1.6创建项目
1.6.1命令创建
django-admin startproject 项目名
django-admin startproject demo01

1.6.2Pycharm创建
注意选择自己刚才创建的Django独立环境

1.7启动项目
1.7.1命令行启动
在和 manage.py同目录下执行
# 指定端口
# python manage.py runserver 8080
# 默认是 8000
python manage.py runserver
默认启动在 http://127.0.0.1:8000/

1.7.2Pycharm启动
也可以配置端口等参数

然后点击运行

1.7.3访问
运行成功后可以使用浏览器访问
http://127.0.0.1:8000/

1.8Django应用(Application)
1.8.1概念
在 Django 中:应用(App)是一个具有独立业务功能的模块。
一个 Django 项目通常由 多个 App 组成,每个 App 负责一类业务。
一个 App 通常包含:
| 组件 | 作用 |
|---|---|
| models.py | 数据库模型 |
| views.py | 业务逻辑 |
| urls.py | URL路由 |
| templates | HTML模板 |
| admin.py | 后台管理 |
| forms.py | 表单 |
| tests.py | 测试 |
| signals.py | 信号 |
| middleware | 中间件 |
所以:App = 一个完整业务模块
Django 项目 与 应用关系
Django项目
│
├── user (用户系统)
├── order (订单系统)
├── goods (商品系统)
├── promotion (促销系统)
├── logistics (物流系统)
关系可以理解为:
Project(项目)
↓
多个 App(应用)
1.8.2命令创建App
当前文件夹必须有 manage.py
# 示例
# python manage.py startapp user
python manage.py startapp APP名字

Pycharm创建完成后没有目录可以重新加载

在settings.py 配置自己的App名称,就是我们刚才创建的名称

1.8.3Pycharm创建App
在 工具 中选择 运行manage.py任务
然后输入命令
# 示例
# startapp shop
startapp 项目名称

在settings.py 会默认配置创建App的具体配置路径

1.9Django项目文件介绍
demo02 # 主项目名 必须
├── demo02 # 存放 Django 项目的基本配置的文件夹 和你的项目名同名 必须
│ ├── __init__.py # 初始化项目需要加载的代码 后面会写
│ ├── __pycache__ # Python解释器给Django项目生成的缓存文件 不用管
│ │ ├── __init__.cpython-310.pyc # Python解释器缓存文件
│ │ └── settings.cpython-310.pyc # settings 更改后的缓存
│ ├── asgi.py # ASGI服务器入口,用于异步部署 本地开发不用动
│ ├── settings.py # Django的项目配置文件
│ ├── urls.py # URL路由映射文件
│ └── wsgi.py # WSGI服务器入口,用于生产环境部署 本地开发不用动
├── manage.py # 加载Django项目所有配置 并帮助我们启动项目
├── templates # HTML模板文件夹
└── user # 自己创建的 APP 的名字
├── __init__.py # APP初始化文件
├── admin.py # 后台管理注册内容,后面写
├── apps.py # 当前APP的默认配置,不要随便改
├── migrations # 数据库迁移记录文件夹 MySQL 将 Python代码定义的数据库结构转换成SQL语句
│ └── __init__.py # 必须存在
├── models.py # 数据库模型文件,通过 Python 定义字段和表结构
├── tests.py # Django测试文件,一般不用管
└── views.py # 自己写的业务逻辑的地方

2.Django视图三板斧
在 Django 的 views.py 中,最常用的返回方式有三种:
| 方法 | 用途 | 返回 |
|---|---|---|
| HttpResponse | 返回字符串 | 文本 |
| render | 返回 HTML 页面 | 模板 |
| redirect | 跳转页面 | 新 URL |
2.1render
2.1.1简介
render 是一个 快捷函数,用于 将模板渲染成 HTML 响应 并返回给浏览器。
它是 HttpResponse 的封装,本质上最终还是返回一个 HttpResponse 对象。
参数说明
| 参数 | 说明 |
|---|---|
request |
必须,当前请求对象,模板渲染时可以用它访问 request.user、request.session 等 |
template_name |
模板路径,如 'index.html',可以相对 app templates 目录 |
context |
字典,模板上下文,用于传递数据给模板 |
content_type |
可选,设置返回的 Content-Type,比如 'text/html; charset=utf-8' |
status |
可选,设置 HTTP 状态码,比如 404、500 |
using |
可选,指定模板引擎(默认用 settings 中的第一个模板引擎) |
def render(
request, template_name, context=None, content_type=None, status=None, using=None
):
content = loader.render_to_string(template_name, context, request, using=using)
return HttpResponse(content, content_type, status)

2.1.2定义视图函数
创建一个视图函数,返回一个render
一会创建的模版必须是index.html
def index(request):
"""
render的参数
request: 当前 request 对象
template_name 当前需要渲染的模版文件的名字,放在 templates 文件夹里面
context=None jinjia2语法,给前端传送数据 render({"key":"value"}) === context
content_type=None 定义响应文档的类型 text/html / json
status=None 响应状态码 默认 200 ok
using=None 执行使用的是哪个模版引擎 使用默认的
:param request:
:return:
"""
return render(request, "index.html")

2.1.3配置模板系统
在项目的settings.py可配置模版系统
一般DIRS配置空数组和APP_DIRS开启自动扫描,剩下默认即可,Django会自动扫描每个app的templates文件夹

TEMPLATES的参数说明
| 配置 / 功能 | 说明 | 示例 / 注意点 |
|---|---|---|
| BACKEND | 模板引擎 | Django 默认:django.template.backends.django.DjangoTemplatesJinja2: django.template.backends.jinja2.Jinja2 |
| DIRS | 项目级模板目录 | [BASE_DIR / 'templates'],空时依赖 APP_DIRS |
| APP_DIRS | 自动扫描 app 模板 | True → 扫描每个 app 的 templates 文件夹 |
| context_processors | 全局模板变量 | request、user、messages 等 |
| render() | 渲染模板并返回 HttpResponse | render(request, 'index.html', {'key': value})自动查找模板、渲染 context + request |
| render 流程 | URL → view → render → template → HttpResponse | 1. 查找模板(DIRS + APP_DIRS) 2. 渲染模板(context + request) 3. 返回 HTML |
2.1.4定义模版文件
模版文件就是一个简单的html,需要留意的是相对导入的静态文件和user同级,所以需要俩次../
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index</title>
{# ======================================================#}
{# 这是相对路劲 #}
{# ======================================================#}
<script src="../../static/jquery.min.js"></script>
<link href="../../static/bootstrap.min.css" rel="stylesheet">
<script src="../../static/bootstrap.min.js"></script>
</head>
<body>
<h1>这是 index 页面</h1>
</body>
</html>

2.1.5注册路由
urlpatterns = [
path('index/', index)
]
为什么
index/要加/?
- Django 官方推荐: Django 的路由系统默认认为 URL 以
/结尾是规范 URL- 方便自动重定向: 设置
APPEND_SLASH = True(默认值)时,访问/index会自动重定向到/index/- 符合 REST 风格: URL 以
/结尾表示“资源”,无/结尾通常被认为是文件或静态资源

2.1.6测试
启动项目,打开8000端口(默认),页面显示我们配置的所有路由
http://127.0.0.1:8000/

访问
http://127.0.0.1:8000/index/

2.2redirect
2.2.1简介
redirect() 用于 返回一个 HTTP 重定向响应(HTTP 302 或指定状态码)
浏览器收到响应后,会自动访问新的 URL
常用于:登录成功跳转、表单提交后跳转、页面迁移等
参数说明
| 参数 | 说明 | 例子 / 备注 |
|---|---|---|
to |
跳转目标,可传多种类型 | - URL 字符串:'/index/'- 视图函数 name:'index'- 模型对象(会调用 get_absolute_url()) |
*args |
可选参数,用于 to 是视图 name 时传递位置参数 |
对应 URL 配置中的 <int:pk> 等参数,如 redirect('page', 2) → /page/2/ |
**kwargs |
可选关键字参数,用于 to 是视图 name 时传递命名参数 |
对应 URL 配置中的 <int:page> 等参数,如 redirect('page', page=2) |
permanent |
是否永久重定向 | False → 默认 302 临时重定向True → 301 永久重定向 |
preserve_request |
是否保留原请求方法和 body | False → 默认True → 保留 GET/POST 等请求方法和请求体 |
def redirect(to, *args, permanent=False, preserve_request=False, **kwargs):
redirect_class = (
HttpResponsePermanentRedirect if permanent else HttpResponseRedirect
)
return redirect_class(
resolve_url(to, *args, **kwargs),
preserve_request=preserve_request,
)

临时重定向和永久重定向
临时重定向(响应状态码:302)和永久重定向(响应状态码:301)对普通用户来说是没什么区别的,它主要面向的是搜索引擎的机器人。
- A页面临时重定向到B页面,那搜索引擎收录的就是A页面。 # 可以通过 回退回去
- A页面永久重定向到B页面,那搜索引擎收录的就是B页面。 # 不可以通过 回退回去
2.2.2定义视图函数
访问func跳转到https://www.baidu.com
def func(request):
"""
redirect(to, *args, permanent=False, **kwargs)
to : 要跳转的地址
写法示例:
1. 完整地址: https://www.baidu.com
2. 绝对路径: /index/ 最终跳转 http://127.0.0.1:8000/index/
3. 相对路径: index/ 最终跳转 http://127.0.0.1:8000/func/index/
"""
return redirect("https://www.baidu.com")

2.2.3注册路由
在urls.py注册路由
urlpatterns = [
path('admin/', admin.site.urls),
path('index/', index),
path('func/', func),
]

2.2.4测试
访问func会自动跳转到https://www.baidu.com/
http://127.0.0.1:8000/func/

2.3HttpResponse
2.3.1简介
HttpResponse:
-
Django 用来 返回 HTTP 响应的类
-
可以直接返回字符串、HTML、JSON 等内容
-
render()和redirect()本质上都是返回HttpResponse的子类
参数说明
| 参数 | 说明 | 示例 / 备注 |
|---|---|---|
content |
响应内容,字符串或字节 | "Hello World" |
content_type |
MIME 类型,告诉浏览器内容类型 | "text/html"(默认)、"application/json" |
status |
HTTP 状态码 | 200、404、500 |
reason |
HTTP 状态原因短语(一般不用) | "Not Found" |
charset |
编码,默认 utf-8 |
"utf-8" |
headers |
响应头,字典形式,可设置 Content-Type、Location、Cache-Control 等 | {'X-Test-Header': 'Demo', 'Content-Type': 'text/plain'} |
HttpResponse(
content=b'',
content_type=None,
status=200,
reason=None,
charset=None,
)
class HttpResponse(HttpResponseBase):
streaming = False
def __init__(self, content=b"", *args, **kwargs):
super().__init__(*args, **kwargs)
# Content is a bytestring. See the `content` property methods.
self.content = content
...
class HttpResponseBase:
status_code = 200
def __init__(
self, content_type=None, status=None, reason=None, charset=None, headers=None
):
self.headers = ResponseHeaders(headers)
self._charset = charset
if "Content-Type" not in self.headers:
if content_type is None:
content_type = f"text/html; charset={self.charset}"
self.headers["Content-Type"] = content_type
elif content_type:
raise ValueError(
"'headers' must not contain 'Content-Type' when the "
"'content_type' parameter is provided."
)
self._resource_closers = []
# This parameter is set by the handler. It's necessary to preserve the
# historical behavior of request_finished.
self._handler_class = None
self.cookies = SimpleCookie()
self.closed = False
if status is not None:
try:
self.status_code = int(status)
except (ValueError, TypeError):
raise TypeError("HTTP status code must be an integer.")
if not 100 <= self.status_code <= 599:
raise ValueError("HTTP status code must be an integer from 100 to 599.")
self._reason_phrase = reason
...

2.3.2定义视图函数
这里就简单返回字符串
def aaa(request):
return HttpResponse("aaa")

2.3.3注册路由
在urls.py注册aaa/
urlpatterns = [
path('admin/', admin.site.urls),
path('index/', index),
path('func/', func),
path('aaa/', aaa)
]

2.3.4测试
访问http://127.0.0.1:8000/aaa/

3.Django静态文件系统
3.1静态文件
静态文件的分类
- 模板文件
templates文件夹- 存放 HTML 页面源码
- 资源文件
- static 文件夹
- css 样式文件
- js 脚本文件
- 第三方前端框架 (bootstrap)
- 图片文件
- 视频文件
- static 文件夹
静态文件的目录
static
├── css
│ └── style.css
├── img
│ └── logo.png
├── js
│ └── index.js
├── plugins
│ ├── bootstrap
│ │ ├── bootstrap.min.css
│ │ └── bootstrap.min.js
│ └── jQuery
│ └── jquery.min.js
└── video
3.2静态文件语法
3.2.1相对路径
<script src="../../static/jquery.min.js"></script>
<link href="../../static/bootstrap.min.css" rel="stylesheet">
<script src="../../static/bootstrap.min.js"></script>

3.2.2配置STATIC_URL和STATICFILES_DIRS
地址: https://docs.djangoproject.com/en/5.2/howto/static-files/

配置STATIC_URL、STATICFILES_DIRS,开发环境可以忽略STATIC_ROOT
# ======================================================
# 静态文件 URL 访问路径
# ======================================================
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
# 作用:定义浏览器访问静态资源的 URL 前缀
# 例如:
# - <link rel="stylesheet" href="{% static 'css/style.css' %}">
# - http://127.0.0.1:8000/static/css/style.css
STATIC_URL = 'static/'
# ======================================================
# 项目级静态文件目录
# ======================================================
# 作用:告诉 Django 在 开发环境 下从哪些目录寻找静态文件。
# - 这里的 BASE_DIR / "static" 是你项目根目录下的 static 文件夹。
# - 开发时,Django 的 runserver 会根据这个配置找到静态资源并直接提供给浏览器。
STATICFILES_DIRS = [
BASE_DIR / "static"
]
# ======================================================
# 生产环境静态文件收集目录
# ======================================================
# 作用:告诉 Django 生产环境 下 collectstatic 命令要把所有静态文件收集到哪个目录。
# 在生产环境,通常不让 Django 直接提供静态文件,而是交给 Nginx/Apache 来服务。
# 执行 python manage.py collectstatic
# Django 会把 STATICFILES_DIRS 中的静态文件 和 每个 app 里的 app_name/static/ 文件
# 收集到 STATIC_ROOT 指定的目录(这里是 staticfiles)。
STATIC_ROOT = BASE_DIR / "staticfiles"

3.2.3语法
{# 第一句 : 加载静态文件语法系统 #}
{% load static %}
{# ======================================================#}
{# 第二步: 导入静态文件 #}
{# ======================================================#}
<link rel="stylesheet" href="{% static 'plugins/bootstrap/bootstrap.min.css' %}">
<script src="{% static 'plugins/bootstrap/bootstrap.min.js' %}"></script>
<img src="{% static 'img/2.png' %}">

3.2.4测试
访问http://127.0.0.1:8000/index/,查看静态文件是否正常导入了

4.request对象
4.1简介
这里的 request:
- 类型:
HttpRequest - 来源:Django 在接收到 HTTP 请求后自动创建
- 作用:让你在视图里访问请求的所有数据
def index(request):
return render(request, "index.html")

常用属性
| 属性名 | 类型 | 说明 | 示例 |
|---|---|---|---|
request.method |
str | 请求方式 | "GET" / "POST" |
request.GET |
QueryDict | URL 查询参数 | ?a=1&b=2 |
request.POST |
QueryDict | 表单提交数据 | <form> 提交 |
request.body |
bytes | 原始请求体(JSON等) | json.loads() |
request.path |
str | 请求路径 | /index/ |
request.headers |
dict-like | 请求头 | User-Agent |
request.META |
dict | 原始请求信息 | IP、端口等 |
request.FILES |
MultiValueDict | 上传文件 | 文件对象 |
request.COOKIES |
dict | 客户端 cookies | sessionid |
request.session |
dict-like | 会话数据 | 存登录信息 |
request.user |
User对象 | 当前登录用户 | is_authenticated |
常用方法
| 方法名 | 返回值 | 说明 | 示例 |
|---|---|---|---|
request.GET.get() |
str | 获取 GET 参数 | .get("name") |
request.POST.get() |
str | 获取 POST 参数 | .get("pwd") |
request.build_absolute_uri() |
str | 完整 URL | http://xx/index/?a=1 |
request.get_full_path() |
str | 路径 + 参数 | /index/?a=1 |
request.get_host() |
str | 主机地址 | 127.0.0.1:8000 |
request.is_secure() |
bool | 是否 HTTPS | True / False |
4.2定义视图函数
创建login视图,可接收Get或Post请求
@csrf_exempt
def login(request):
print(dir(request))
# 获取当前请求方式
print(request.method)
if request.method == "GET":
# 请求参数 字典类型
# "GET /login/?username=peng&password=123456&hobby=music&hobby=run&hobby=dance"
print(request.GET)
username = request.GET.get("username")
password = request.GET.get("password")
hobbby = request.GET.getlist("hobby")
print(f"GET请求 username={username} password={password} hobbby={hobbby}")
else:
# 请求参数 字典类型
print(request.POST)
username = request.POST.get("username")
password = request.POST.get("password")
hobbby = request.POST.getlist("hobby")
print(f"POST请求 username={username} password={password} hobbby={hobbby}")
return render(request, "login.html")

4.3注册路由
注册login路由
urlpatterns = [
path('login/', login)
]

4.4定义模版文件
登录页面负责发送Get或者Post请求,
- method="get": 发送get请求
- method="post": 发送post请求

4.5测试Get请求
点击submit发送了get请求
dir(request): 查看 request 对象所有属性和方法request.method: 获取 HTTP 请求方式request.GET.get(): 获取 GET 请求参数

4.6测试Post请求
Django POST 请求报 403,是因为 CsrfViewMiddleware 拦截了缺少 CSRF token 的请求,用于防御跨站请求伪造。
-
GET 请求不检查 CSRF,只检查会修改数据的请求(POST/PUT/DELETE/PATCH)
-
开发阶段可以临时关闭 CSRF 或用
csrf_exempt,但生产环境一定要开启
CSRF(Cross-Site Request Forgery)跨站请求伪造:
- 是一种攻击方式:攻击者伪造用户身份,向网站提交请求
- 比如用户登录了网站 A,攻击者在网站 B 中偷偷发起请求,利用用户的登录状态进行操作
Django 内置了 CsrfViewMiddleware:
- 默认在
MIDDLEWARE中启用:django.middleware.csrf.CsrfViewMiddleware - 作用:对 修改数据的请求(POST / PUT / DELETE)进行 CSRF 校验

修改login.html的method="post",然后点击点击submit发送了post请求
request.POST.get: 获取 POST 请求参数

5.Django连接Mysql
5.1docker安装mysql:latest
我这里使用docker搭建测试环境比较方便
下载镜像
在 Docker 里,:latest 是 一个镜像标签(tag),表示“该镜像库中官方标记的最新稳定版本”。它不是固定版本号,而是一个 动态指向当前最新发布的版本。
# 或者 mysql:8.0
docker pull mysql:latest

查看镜像有没有下载成功
docker images

创建mysql需要的三个目录:
data: 数据持久化
conf: 自定义配置文件
init: 初始化 SQL 脚本(第一次启动时执行)
# 删除文件夹 rm -rf 文件夹
# 创建文件夹 并设置 读写权限
mkdir -p /root/eshop/mysql8/data
mkdir -p /root/eshop/mysql8/conf
mkdir -p /root/eshop/mysql8/init
chmod -R 777 /root/eshop/mysql8/data
chmod -R 777 /root/eshop/mysql8/conf
chmod -R 777 /root/eshop/mysql8/init

编辑配置文件
vim /root/eshop/mysql8/conf/conf.d
编辑完成之后可以使用 cat /root/eshop/mysql8/conf/conf.d查看
conf.d配置如下
[client]
default_character_set=utf8mb4
[mysql]
default_character_set=utf8mb4
[mysqld]
character_set_server=utf8mb4
collation_server=utf8mb4_unicode_ci
init_connect='SET NAMES utf8mb4'

运行镜像:
--name mysql8:容器名称--restart=always:开机自启动-p 3306:3306:映射宿主机3306端口到容器3306端口-v /root/eshop/mysql8/data:/var/lib/mysql:数据目录映射到宿主机,持久化数据库-v /root/eshop/mysql8/conf:/etc/mysql/conf.d:配置文件目录映射,可放my.cnf片段-v /root/eshop/mysql8/init:/docker-entrypoint-initdb.d:初始化SQL脚本目录-e MYSQL_ROOT_PASSWORD=root123:设置 root 密码-e MYSQL_DATABASE=django_demo:启动时自动创建数据库-e MYSQL_USER=django:普通用户-e MYSQL_PASSWORD:普通用户密码-d:后台运行
docker run -d \
--name mysql8 \
--restart=always \
-p 3306:3306 \
-v /root/eshop/mysql8/data:/var/lib/mysql \
-v /root/eshop/mysql8/conf:/etc/mysql/conf.d \
-v /root/eshop/mysql8/init:/docker-entrypoint-initdb.d \
-e MYSQL_ROOT_PASSWORD=root \
-e MYSQL_DATABASE=django_demo \
mysql:latest

可以查看容器是否运行
docker ps

5.2配置数据库
在settings.py配置数据库链接
# 数据库配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', # 数据库引擎
'NAME': 'django_demo', # 数据库名
'USER': 'root', # 数据库用户名
'PASSWORD': 'root', # 数据库密码
'HOST': '192.168.188.180', # 数据库主机
'PORT': '3306', # 数据库端口
}
}

5.3No module named 'MySQLdb'
数据库连接配置好后启动项目可能会报错: Django想用 MySQL 驱动 MySQLdb,但你环境里没有
Watching for file changes with StatReloader
Exception in thread django-main-thread:
Traceback (most recent call last):
File "../miniconda\package\envs\django_env\Lib\site-packages\django\db\backends\mysql\base.py", line 16, in <module>
import MySQLdb as Database
ModuleNotFoundError: No module named 'MySQLdb'

第一种方式: 安装mysqlclient,官方推荐
直接安装mysqlclient,官方推荐,我使用的就是这种方式
pip install mysqlclient

第二种方式安装: PyMySQL
安装PyMySQL,我这里记录一下这种方式
pip install mysqlclient
项目的 __init__ 文件进行导入
import pymysql
pymysql.install_as_MySQLdb()

5.4创建数据库
DATABASES配置了数据库,如果数据库不存在可能会报错,所以需要提前创建数据库

我这里是在运行容器的时候就创建了数据库

5.6启动项目
项目正常启动即可

5.5Django后台管理系统
项目成功启动后,我们之前在配置和路由中添加了Django后台管理系统

如果连接Mysql成功,他自己会迁移后台管理的相关数据库

6.Django ORM
6.1简介
ORM (Object-Relational Mapping)
本质:把 数据库表映射为 Python 类,把 表记录映射为 对象实例,从而用 Python 代码操作数据库,而不直接写 SQL。
映射关系
- 数据库表 -> Python 类
- 数据库字段 -> Python 属性
- 数据库记录 -> Python 实例
Django ORM 核心特点:
- 模型驱动数据库:通过
models.py定义表结构。 - 查询集(QuerySet):延迟执行、链式操作。
- 数据库无关:可以切换 MySQL、PostgreSQL、SQLite 等。
- 自动生成 SQL:隐藏底层 SQL,实现高效开发。
- 支持高级功能:关联查询、聚合、事务、约束、索引等。
字段类型映射
| Django Field | SQL 类型 |
|---|---|
| CharField | VARCHAR |
| IntegerField | INT |
| EmailField | VARCHAR + 校验 |
| DateTimeField | DATETIME |
| BooleanField | BOOLEAN |
常用模型属性
| 属性 | 说明 |
|---|---|
null |
数据库是否允许为空 |
blank |
表单是否允许为空 |
default |
默认值 |
primary_key |
主键 |
unique |
唯一约束 |
choices |
枚举约束 |
db_index |
创建索引 |
6.2定义模型表
定义了教师、课程、部门三个模型
Teacher.department使用ForeignKey建立一对多关系,一个部门可以有多个教师,删除部门时相关教师也会删除(CASCADE)。Teacher.courses使用ManyToManyField建立多对多关系,一个教师可以教授多门课程,一门课程也可以由多个教师教授。
# ======================================================
# 部门模型:Department
# ======================================================
class Department(models.Model):
# 部门名称,最大长度 64 个字符
name = models.CharField(max_length=64)
# 定义对象的字符串表示,方便在 admin 或 shell 中显示
def __str__(self):
return self.name
# ======================================================
# 教师模型:Teacher
# ======================================================
class Teacher(models.Model):
# 教师姓名,最大长度 32 个字符
name = models.CharField(max_length=32)
# 教师年龄,整型字段
age = models.IntegerField()
# 教师邮箱,可选字段(null=True 表示数据库可以为空,blank=True 表示表单可以为空)
email = models.EmailField(null=True, blank=True)
# 外键字段,指向 Department,表示每个教师属于一个部门(一对多关系)
# on_delete=models.CASCADE 表示部门被删除时,关联教师也会被删除
department = models.ForeignKey(Department, on_delete=models.CASCADE)
# 多对多字段,指向 Course,表示教师可以教授多门课程
# blank=True 表示在表单中可不选择课程
courses = models.ManyToManyField('Course', blank=True)
# 定义对象的字符串表示
def __str__(self):
return self.name
# ======================================================
# 课程模型:Course
# ======================================================
class Course(models.Model):
# 课程名称,最大长度 64 个字符
name = models.CharField(max_length=64)
# 定义对象的字符串表示
def __str__(self):
return self.name

6.3生成迁移记录
python manage.py makemigrations:
- 作用:生成迁移文件(migration file),记录你在
models.py中对数据库模型的新增、修改、删除操作。它不会直接修改数据库,只是生成一个 Python 文件,告诉 Django 如何把模型的变化应用到数据库。 - 典型场景:
- 新建模型(表)
- 新增字段
- 修改字段类型
- 删除字段或模型
python manage.py migrate:
- 作用:执行迁移,将
makemigrations生成的迁移文件中的操作真正应用到数据库中,创建或修改表结构。换句话说,它真正改变数据库。 - 典型场景:
- 初始化数据库(第一次迁移)
- 应用新增字段或模型
- 回滚到之前的迁移版本(通过
--fake或migrate <app> <migration>)
执行迁移命令
-
注意自己的环境,我之前使用
conda创建的django环境 -
需要进入在项目目录下,就是
manage.py所在目录执行迁移命令
# 相关命令
# 进入项目目录
cd .\demo03\
# 激活 django 隔离环境
conda activate django_env
# 生成迁移文件
python manage.py makemigrations
# 执行迁移命令
python manage.py migrate
# 日志记录
python manage.py makemigrations
Migrations for 'user':
user\migrations\0010_course_department_teacher.py
+ Create model Course
+ Create model Department
+ Create model Teacher
python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, user
Running migrations:
Applying user.0010_course_department_teacher... OK
(django_env) PS E:\Data\gitCode\peng-python\code\17Django\03ORM+请求声明周期+路由系统\demo03>

迁移文件在migrations目录下

6.4数据库操作
6.4.1删除库
注释或删除models.py的模型类,然后执行迁移命令
python manage.py makemigrations
Migrations for 'user':
user\migrations\0011_remove_teacher_courses_remove_teacher_department_and_more.py
- Remove field courses from teacher
- Remove field department from teacher
- Delete model Course
- Delete model Department
- Delete model Teacher
python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, user
Running migrations:
Applying user.0011_remove_teacher_courses_remove_teacher_department_and_more... OK

6.4.2创建库
上面已经演示创建库了,这里把注释的表模型再取消,然后再执行迁移命令
python manage.py makemigrations
Migrations for 'user':
user\migrations\0012_course_department_teacher.py
+ Create model Course
+ Create model Department
+ Create model Teacher
python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, user
Running migrations:
Applying user.0012_course_department_teacher... OK

6.4.3修改库
给Teacher添加一个性别字段
# ======================================================
# 教师模型:Teacher
# ======================================================
class Teacher(models.Model):
# 教师姓名,最大长度 32 个字符
name = models.CharField(max_length=32)
# 教师年龄,整型字段
age = models.IntegerField()
# 教师邮箱,可选字段(null=True 表示数据库可以为空,blank=True 表示表单可以为空)
email = models.EmailField(null=True, blank=True)
# 外键字段,指向 Department,表示每个教师属于一个部门(一对多关系)
# on_delete=models.CASCADE 表示部门被删除时,关联教师也会被删除
department = models.ForeignKey(Department, on_delete=models.CASCADE)
# 多对多字段,指向 Course,表示教师可以教授多门课程
# blank=True 表示在表单中可不选择课程
courses = models.ManyToManyField('Course', blank=True)
# 性别字段,使用 choices 限定可选值
GENDER_CHOICES = [
('M', '男'),
('F', '女'),
('O', '未选择'), # 可选,用于扩展
]
# max_length=1 表示只存储单个字符,choices 提供可选值
gender = models.CharField(max_length=1, choices=GENDER_CHOICES, default='O')
# 定义对象的字符串表示
def __str__(self):
return self.name
然后再执行迁移命令
python manage.py makemigrations
Migrations for 'user':
user\migrations\0013_teacher_gender.py
+ Add field gender to teacher
python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, user
Running migrations:
Applying user.0013_teacher_gender... OK

6.5字段约束
空值约束
| 配置项 | 作用 |
|---|---|
null=True/False |
数据库层是否允许该字段为 NULL,默认 False(必填) |
blank=True/False |
表单验证层是否允许为空,默认 False(必填) |
唯一性约束
| 配置项 | 作用 |
|---|---|
unique=True |
数据库层保证字段值唯一,不允许重复 |
unique_together = (field1, field2) |
联合唯一约束,两个或多个字段组合必须唯一 |
默认值和自动填充
| 配置项 | 作用 |
|---|---|
default=value |
字段默认值,如果新建对象未提供值则使用该默认值 |
auto_now=True |
对 DateTimeField 类型,每次修改对象时自动更新为当前时间 |
auto_now_add=True |
对 DateTimeField 类型,创建对象时自动设置为当前时间 |
字符串长度与格式
| 配置项 | 作用 |
|---|---|
max_length=N |
最大长度约束(CharField、EmailField、SlugField) |
validators=[...] |
自定义验证器,如正则验证 (RegexValidator) |
数值约束
| 验证器 | 作用 |
|---|---|
MinValueValidator(N) |
限定最小值 |
MaxValueValidator(N) |
限定最大值 |
DecimalField(max_digits, decimal_places) |
指定总位数和小数位数 |
外键和多对多约束
| 配置项 | 作用 |
|---|---|
ForeignKey(Model, on_delete=CASCADE) |
一对多关系,CASCADE 表示父对象删除时子对象也删除 |
ManyToManyField(Model, blank=True) |
多对多关系,blank=True 表示可不选择 |
related_name='xxx' |
设置反向查询名称,方便通过 obj.xxx.all() 查询相关对象 |
选项限制
| 配置项 | 作用 |
|---|---|
choices=[(value, label), ...] |
限定可选值,通常用于性别、状态等字段 |
default=value |
可以与 choices 配合使用,提供默认选项 |
6.6数据操作
6.6.1创建库
创建Student模型
class Student(models.Model):
# 基本信息
student_id = models.CharField(max_length=20, unique=True, verbose_name="学号")
name = models.CharField(max_length=50, verbose_name="姓名")
age = models.PositiveIntegerField(verbose_name="年龄")
# 性别
GENDER_CHOICES = (
('M', '男'),
('F', '女'),
('O', '其他'),
)
gender = models.CharField(max_length=1, choices=GENDER_CHOICES, default='O', verbose_name="性别")
# 联系方式
email = models.EmailField(max_length=100, blank=True, null=True, verbose_name="邮箱")
phone = models.CharField(max_length=20, blank=True, null=True, verbose_name="电话")
# 地址
address = models.TextField(blank=True, null=True, verbose_name="地址")
# 学习信息
gpa = models.DecimalField(max_digits=4, decimal_places=2, default=0.00, verbose_name="GPA") # 小数
enrollment_date = models.DateField(verbose_name="入学日期", null=True, blank=True)
graduation_date = models.DateField(verbose_name="毕业日期", null=True, blank=True)
# 布尔类型
is_active = models.BooleanField(default=True, verbose_name="是否在校")
# 文件类型
# 需要安装 Pillow 这里暂时用不上
# profile_picture = models.ImageField(upload_to='profiles/', null=True, blank=True, verbose_name="头像")
# 自动时间
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
# JSON字段(Django >= 3.1 支持)
extra_info = models.JSONField(default=dict, blank=True, null=True, verbose_name="额外信息")
def __str__(self):
return f"{self.student_id} - {self.name}"
然后执行迁移命令
python manage.py makemigrations
python manage.py migrate
6.6.2配置环境
import os
import django
from django.db import connection
# -------------------------------
# 第一步:配置 Django 环境
# -------------------------------
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo03.settings") # 替换成你的 settings 模块
django.setup() # 初始化 Django 环境,必须在导入模型之前执行
# -------------------------------
# 第二步:导入模型
# -------------------------------
from user.models import Student
# -------------------------------
# 第三步:清空表、重置自增主键(MySQL),方便测试
# -------------------------------
Student.objects.all().delete()
with connection.cursor() as cursor:
cursor.execute("ALTER TABLE user_student AUTO_INCREMENT = 1;")
print("✅ Student 表已清空,ID 已重置为 1")

6.6.3新增
-
create()- 立即创建并保存到数据库
- 返回对象实例
-
实例化 + save():- 先创建 Python 对象(内存中)
- 调用
save()写入数据库
# -----------------------------
# 1️⃣ 新增数据
# -----------------------------
# 方法一:直接 create(常用)
student1 = Student.objects.create(student_id="S001", name="peng1", age=18, gender="M")
print(f"新增学生:{student1} ")
student1 = Student.objects.create(student_id="S002", name="tom", age=19, gender="M")
print(f"新增学生:{student1} ")
student1 = Student.objects.create(student_id="S003", name="alice", age=20, gender="F")
print(f"新增学生:{student1} ")
student1 = Student.objects.create(student_id="S004", name="marry", age=21, gender="F")
print(f"新增学生:{student1} ")
# 方法二:实例化 + save(少用)
student2 = Student(student_id="S005", name="peng2", age=22, gender="M")
student2.save()
student3 = Student(student_id="S006", name="peng3", age=23, gender="M")
student3.save()
print(f"新增学生:{student2} ")
6.6.4查询
all()- 获取所有数据- 返回 QuerySet(可迭代)
- 遍历即可访问每条记录
get()- 获取单条数据- 返回单个对象
- 若不存在 → 抛出
DoesNotExist - 若多条匹配 → 抛出
MultipleObjectsReturned
filter()- 条件查询- 返回 QuerySet(列表形式,可为空)
- 支持模糊查询:name__contains
- 获取首记录:first()
- 获取尾记录:last()
exclude()- 排除符合条件的数据- 返回 QuerySet
- 排除指定条件的数据
# -----------------------------
# 2️⃣ 查询数据
# -----------------------------
# 方法一:all() - 获取所有数据
print("\n查询所有学生:")
all_students = Student.objects.all()
for s in all_students:
print(f"{s.id} - {s.student_id} - {s.name} - {s.age} - {s.gender}")
# 方法二:get() - 获取单挑数据
print("\n查询指定 id 学生:")
try:
student3 = Student.objects.get(id=all_students[0].id)
print(f"id={all_students[0].id}: {student3.name} - {student3.age}")
except Student.DoesNotExist:
print(f"id={all_students[0].id} 的学生不存在")
except Student.MultipleObjectsReturned:
print(f"id={all_students[0].id} 的学生不唯一")
# 方法三:filter() - 获取 QuerySet(列表)
print(f"\nfilter(name='peng1')")
student_list = Student.objects.filter(name="peng1")
for s in student_list:
print(f"{s.id} - {s.name} - {s.age}")
# name__contains: 包含关系
student_list = Student.objects.filter(name__contains="peng")
print(f"\nfilter(name__contains='peng'):{student_list}")
print(f"第一条: {student_list.first()}")
print(f"最后一条: {student_list.last()}")
# 方法四:exclude() - 排除符合条件的数据
not_peng_list = Student.objects.exclude(name="peng1")
print("\nexclude(name='peng1'):")
for s in not_peng_list:
print(f"{s.id} - {s.name} - {s.age}")
6.6.5删除
-
filter().delete() - 删除符合条件的所有记录
- 返回值:
-
deleted_count→ 删除的总条数 -
dict→ 每个模型删除数量 -
推荐批量删除使用
-
- 返回值:
-
先获取对象再
delete()- 删除单条对象DoesNotExist→ 对象不存在MultipleObjectsReturned→ 多条匹配(如果用get())
# -----------------------------
# 3️⃣ 删除数据
# -----------------------------
# 方式一:filter().delete()
# deleted_count 总共删除的记录数
# dict 每个模型删除的具体数量
deleted_count, dict = Student.objects.filter(name="peng2").delete()
print(f"\n删除符合 name='dream01' 的学生数量:{deleted_count}")
# 方式二:先获取对象再 delete()
try:
student_obj = Student.objects.get(id=6)
student_obj.delete()
print(f"id=6 的学生删除成功")
except Student.DoesNotExist:
print(f"id=6 的学生不存在,无法删除")
print("查看所有学生:")
all_students = Student.objects.all()
for s in all_students:
print(f"{s.id} - {s.student_id} - {s.name} - {s.age} - {s.gender}")
6.6.6修改
- filter().update() - 批量修改
- 直接修改符合条件的记录
- 立即写入数据库,不触发
save()方法和信号 - 返回值:修改的记录数量
- 对象修改属性 +
save()- 单条修改- 先获取对象 → 修改属性 → 保存
- 可触发
save()方法和模型信号 - 异常处理:
DoesNotExist→ 对象不存在MultipleObjectsReturned→ 多条匹配(若使用get())
# -----------------------------
# 4️⃣ 修改数据
# -----------------------------
# 方法一:filter().update() - 批量修改
updated_count = Student.objects.filter(name="peng1").update(age=28)
print(f"\n修改 age=20 的学生数量:{updated_count}")
# 方法二: 对象修改属性 + save()
try:
student_obj = Student.objects.get(id=1)
student_obj.name = "peng111"
student_obj.age = 29
student_obj.save()
print(f"修改成功:id={student_obj.id} name={student_obj.name} age={student_obj.age}")
except Student.DoesNotExist:
print(f"id=1 的学生不存在,无法修改")
print("查看所有学生:")
all_students = Student.objects.all()
for s in all_students:
print(f"{s.id} - {s.student_id} - {s.name} - {s.age} - {s.gender}")
6.6.7总结
| 操作类型 | 方法/语法 | 返回值 | 特点 | 适用场景 | 异常/注意事项 |
|---|---|---|---|---|---|
| 新增 | Model.objects.create(**fields) |
对象实例 | 一行完成创建并写入数据库 | 快速新增单条数据 | 无法先做逻辑判断 |
| 新增 | obj = Model(**fields); obj.save() |
对象实例 | 可在保存前修改属性或做逻辑判断 | 需要先处理逻辑 | 忘记调用 save() 数据不会写入 |
| 查询 | Model.objects.all() |
QuerySet | 获取所有记录 | 遍历全部数据 | 返回 QuerySet,可为空 |
| 查询 | Model.objects.get(**条件) |
单对象 | 获取单条记录 | 查询唯一记录 | 不存在 → DoesNotExist;多条 → MultipleObjectsReturned |
| 查询 | Model.objects.filter(**条件) |
QuerySet | 条件查询,可模糊 | 查询多条数据 | 可为空,不会抛异常 |
| 查询 | Model.objects.exclude(**条件) |
QuerySet | 排除符合条件数据 | 查询非指定条件数据 | 可与 filter 链式组合 |
| 删除 | Model.objects.filter(**条件).delete() |
(删除条数, dict) |
批量删除 | 批量删除数据 | 立即删除,无法恢复 |
| 删除 | obj = Model.objects.get(...); obj.delete() |
删除条数 | 删除单条对象 | 单条删除,可先处理逻辑 | 需处理 DoesNotExist 异常 |
| 修改 | Model.objects.filter(**条件).update(**fields) |
修改条数 | 批量修改,立即写入,不触发 save() | 批量更新数据 | 不触发信号 |
| 修改 | obj = Model.objects.get(...); obj.attr = val; obj.save() |
对象实例 | 单条修改,触发 save() 和信号 | 单条更新或触发逻辑 | 需处理 DoesNotExist 异常 |
7.Django请求生命周期
Django 处理一个 HTTP 请求的完整流程如下:
浏览器请求
↓
WSGI/ASGI Server(如 gunicorn / uwsgi)
↓
Django入口(wsgi.py / asgi.py)
↓
中间件(请求阶段)
↓
URL路由匹配(urls.py)
↓
视图函数 / 类视图(views)
↓
(可选)ORM操作数据库
↓
模板渲染(templates)
↓
返回 HttpResponse
↓
中间件(响应阶段)
↓
WSGI/ASGI Server
↓
浏览器响应
8.Django路由系统
8.1简介
Django 使用 URLconf(URL configuration)实现:URL → View 的映射
本质:urlpatterns = [路径 → 视图函数]
执行流程:浏览器请求 → Django → urlpatterns 顺序匹配 → 命中 → 执行 view
重点:按“从上到下”顺序匹配
Django 路由语法演进
-
Django 1.x(旧版本)
完全基于正则表达式 from django.conf.urls import url urlpatterns = [ url(r'^login/$', lambda request: None), ] """ -
Django 2.x+(主流)
推荐使用 path,简单清晰
from django.urls import path, re_path
urlpatterns = [
path('login/', lambda request: None), # 推荐
re_path(r'^login/$', lambda request: None), # 正则写法(兼容)
]
总结:path 简单路径(80%场景),re_path 复杂匹配(正则)
url() 参数详解(已过时 )
url(regex, view, kwargs=None, name=None)
| 参数 | 说明 | 细节 |
|---|---|---|
regex |
正则表达式 | 必须写正则(类似 re_path) |
view |
视图函数 / 类视图 | 同 path |
kwargs |
额外参数 | 传递给视图 |
name |
路由别名 | 用于反向解析 |
path 参数详解
path(route, view, kwargs=None, name=None)
| 参数 | 说明 | 细节 |
|---|---|---|
route |
URL 路径规则 | 不支持正则,支持 <int:id> 这种“路径转换器” |
view |
视图函数 / 类视图 | FBV 或 as_view() 后的 CBV |
kwargs |
额外参数(字典) | 会作为参数传递给视图 |
name |
路由别名 | 用于 reverse 和 {% url %} |
re_path参数详解
re_path(route, view, kwargs=None, name=None)
| 参数 | 说明 | 细节 |
|---|---|---|
route |
正则表达式 | 必须用 r'',支持分组 |
view |
视图函数 / 类视图 | 同 path |
kwargs |
额外参数 | 同 path |
name |
路由别名 | 同 path |
8.2路由匹配机制
创建2个测试视图
def add(request):
return HttpResponse("add")
def add111(request):
return HttpResponse("add111")
注册路由
urlpatterns = [
# **************************************
# 使用 re_path add111 会被匹配到 add
# 也就是访问 http://127.0.0.1:8000/add111
# 会被匹配到 http://127.0.0.1:8000/add
re_path('add', add),
re_path('add111', add111)
]

访问
http://127.0.0.1:8000/add111/
但是仍匹配到
http://127.0.0.1:8000/add/

除非路劲后加一个分隔符 / ,可正常匹配路由
urlpatterns = [
# **************************************
# 使用 re_path add111 会被匹配到 add
# 也就是访问 http://127.0.0.1:8000/add111
# 会被匹配到 http://127.0.0.1:8000/add
re_path('add/', add),
re_path('add111/', add111)
]

使用path也可解决
Django 默认配置 APPEND_SLASH = True ,当 URL 没匹配成功时,Django 会尝试在末尾加一个 / 再匹配一次
为了避免路由匹配歧义、保持 URL 规范以及兼容中间件,推荐在 path 路由定义时统一以 / 结尾。
urlpatterns = [
path('add/', add),
path('add111/', add111)
]

8.3无名分组
有名分组 / 无名分组:只存在于 正则路由(re_path)中
常见两种写法:
- path() → 简化路由(推荐)
- re_path() → 正则路由(才有分组概念)
不带名字的正则分组就叫:无名分组
在 re_path() 中使用的 正则括号 (),但没有给名字
re_path(r'^user/(\d+)/$', views.detail)
创建视图
def page(request, aaa, *args, **kwargs):
print("这是page...")
# page() takes 1 positional argument but 2 were given
print(aaa, type(aaa)) # 1 <class 'str'>
print(args)
print(kwargs)
if aaa == "3":
return redirect(reverse("home"))
return render(request, "page.html")
注册无名分组路由
urlpatterns = [
re_path("page/(\d+)/", page),
]

访问http://127.0.0.1:8000/page/1/,可以使用任意变量名接收参数

8.4有名分组
在 re_path() 中使用 正则命名括号 (?P<name>...))
(?P<id>\d+)→ 有名分组- 有名分组通过 (?P
...) 给正则参数命名,视图按名字接收,推荐使用。 - 匹配到的内容会 按名称 传给视图函数参数
re_path(r'^user/(?P<id>\d+)/$', views.detail)
创建视图
def page_name(request, page, *args, **kwargs):
print("page_name...")
# page_name() missing 1 required positional argument: 'aaa'
print(page, type(page)) # 1 <class 'str'>
print(args)
# 如果这里使用 page 接收参数了 kwargs 就不会有值
# 但是没有使用 page 接收参数的话 kwargs 就有 {'page': '1'}
print(kwargs) # {'page': '1'}
return render(request, "page.html")
注册有名分组路由
urlpatterns = [
re_path("page_name/(?P<page>\d+)/", page_name)
]

注册路由时?P<page>\d+配置了参数名称和正则,所以视图可以根据page接收参数
当用实际名字接收参数时,**kwargs打印的是空字典

如果我们不用实际名字接收参数时,**kwargs就会存实际的参数
{'page': '1'}

无名分组和有名分组
| 特性 | 无名分组 | 有名分组(命名分组) |
|---|---|---|
| 写法 | (\d+) |
(?P<id>\d+) |
| 参数传递 | 按顺序传给视图函数 | 按名称传给视图函数 |
| 示例 - 单参数 | re_path(r'^user/(\d+)/$', views.detail)视图:def detail(request, id) |
re_path(r'^user/(?P<id>\d+)/$', views.detail)视图:def detail(request, id) |
| 示例 - 多参数 | re_path(r'^user/(\d+)/(\d+)/$', views.detail)视图:def detail(request, id, age) |
re_path(r'^user/(?P<id>\d+)/(?P<age>\d+)/$', views.detail)视图:def detail(request, id, age) |
| 推荐 | 不推荐 | 推荐 |
8.5路由解析
假如我们有一个特别复杂的路由(参数很长,不容易记),然后我们给这个路由起个名字方便前端后端调用
创建视图
def home(request):
return render(request, "home.html")
def zhuye(request):
return render(request, "zhuye.html")
注册路由
urlpatterns = [
path("zhuye", zhuye),
# route:URL 路径字符串
# view:匹配成功后调用的视图函数
# kwargs:额外参数,传给视图函数
# name:给 URL 命名,方便 reverse() 或模板 {% url %} 使用
# 路由解析
path("home/adsadasd/dsadsadsad/sdasadads/sadsadsadsa/", home, name="home")
]

8.5.1前端调用({% url "路由名称" %})
<p><a href="{% url 'home' %}">路径很全的网址</a></p>

8.5.2后端调用(reverse("路由名称"))
reverse("home")

8.6反向解析
8.6.1有名分组
创建有名分组的视图和路由
urls.py
urlpatterns = [
# 有名分组 http://127.0.0.1:8000/parse_name/1/
re_path("^parse_name/(?P<id>\d+)/", parse_name, name="parse_name"),
re_path("^parse_name_redirect/(?P<id>\d+)/", parse_name_redirect, name="parse_name_redirect")
]
views.py
def parse_name(request, *args, **kwargs):
print(args) # ()
print(kwargs) # {'id': '1'}
# args : 按照位置传递参数
# kwargs :按照关键字传递参数
id = kwargs.get("id")
# http://127.0.0.1:8000/parse_name_redirect/(?P<id>\d+)/
return redirect(reverse("parse_name_redirect", args=(id,)))
# 如果是按照关键字传递参数建议 同名传
# return redirect(reverse("parse_name_redirect", kwargs={"name": id}))
# Reverse for 'parse_name_redirect' with keyword arguments '{'name': '1'}' not found. 1 pattern(s) tried: ['parse_name_redirect/(?P<id>\\d+)/']
# return redirect(reverse("parse_name_redirect", kwargs={"id": id}))
def parse_name_redirect(request, *args, **kwargs):
print(args) # ()
print(kwargs) # {'id': '1'}
return HttpResponse("parse_name_redirect")

8.6.1.1前端的反向解析
有名分组 / 无名分组:在 {% url %} 后面直接传参数
本质:通过 name 生成 URL(反向解析)
<p>
http://127.0.0.1:8000/parse_name/1/
</p>
<p>
<a href="/parse_name/1/">有名分组</a>
</p>

8.6.1.2位置传参(args)
在Django 5.2版本中有名分组不能通过位置(args)传参数

8.6.1.3关键字传参(kwargs)
可以通过关键字传参

kwargs 里的 key,必须和路由中定义的参数名一模一样,否则会报错
Reverse for 'parse_name_redirect' with keyword arguments '{'name': '1'}' not found. 1 pattern(s) tried: ['parse_name_redirect/(?P<id>\\d+)/']

8.6.2无名分组
创建无名分组的视图和路由
urls.py
urlpatterns = [
# 无名分组
re_path("^parse_no_name/(\d+)/", parse_no_name, name="parse_no_name"),
re_path("^parse_no_name_redirect/(\d+)/", parse_no_name_redirect, name="parse_no_name_redirect"),
]
views.py
# 无名分组
def parse_no_name(request, *args, **kwargs):
print(args) # ('1',)
print(kwargs) # {}
id = args[0]
return redirect(reverse("parse_no_name_redirect", args=(id,)))
# Reverse for 'parse_no_name_redirect' with keyword arguments '{'name': '1'}' not found. 1 pattern(s) tried: ['parse_no_name_redirect/(\\d+)/']
# return redirect(reverse("parse_no_name_redirect", kwargs={"name":id}))
def parse_no_name_redirect(request, *args, **kwargs):
print(args) # ('1',)
print(kwargs) # {}
return HttpResponse("parse_no_name_redirect")

8.6.2.1前端的反向解析
有名分组 / 无名分组:在 {% url %} 后面直接传参数
本质:通过 name 生成 URL(反向解析)
<p>
<a href="/parse_no_name/1/">无名分组</a>
</p>
<p>
<a href="{% url 'parse_no_name' "9" %}">无名分组</a>
</p>
8.6.2.2位置传参(args)
无名分组只能通过位置传参(args),不能关键字传参
无名分组没有 key,因此只能用 args;kwargs 在语义上就不成立

8.6.2.3关键字传参(kwargs不支持)
无名分组不支持关键字传参
Reverse for 'parse_no_name_redirect' with keyword arguments '{'id': '9'}' not found. 1 pattern(s) tried: ['parse_no_name_redirect/(\\d+)/']

8.7路由分发
当项目变大时:
- app 越来越多(user / shop / order / payment…)
- 路由越来越多(几十甚至上百条)
如果全写在一个 urls.py:
urlpatterns = [
path("shop/order/", ...),
path("shop/buy/", ...),
path("user/login/", ...),
path("user/register/", ...),
...
]
问题:
- 可读性差
- 难维护
- 高耦合(所有 app 混在一起)
我们先创建一个shop模块,我这里使用的是Pycharm创建的,可以自动导入app(settings.py/INSTALLED_APPS)
如果使用python manage.py startapp记得手动导入app(settings.py/INSTALLED_APPS)
startapp shop

并创建2个视图
def order(request):
return HttpResponse("order")
def buy(request):
return HttpResponse("buy")

8.7.1全部写在主urls.py
特点:
- 所有路由写在一个文件
- 简单直观
- 路由多了非常混乱(不推荐)
urlpatterns = [
# 方案一:全部写在主 urls.py(最原始)
# shop 模块
path("shop/order/", order, name="order"),
path("shop/buy/", buy, name="buy"),
# user 模块
re_path("user/parse_name/(?P<id>\d+)/", parse_name, name="parse_name"),
re_path("^parse_name_redirect/(?P<id>\d+)/", parse_name_redirect, name="parse_name_redirect"),
re_path("user/parse_no_name/(\d+)/", parse_no_name, name="parse_no_name"),
re_path("^parse_no_name_redirect/(\d+)/", parse_no_name_redirect, name="parse_no_name_redirect")
]

8.7.2拆分成多个列表
特点:
- 按 app 分类
- 比方案一清晰
- 仍然集中在一个文件
# 方案二:拆分成多个列表(仍在主文件)
shop_urlpatterns = [
path("shop/order/", order, name="order"),
path("shop/buy/", buy, name="buy"),
]
user_urlpatterns = [
re_path("^user/parse_name_redirect/", parse_name_redirect, name="parse_name_redirect"),
re_path("^user/parse_no_name_redirect/", parse_no_name_redirect, name="parse_no_name_redirect"),
]
urlpatterns += shop_urlpatterns
urlpatterns += user_urlpatterns

8.7.3每个app 单独urls.py
特点:
- 每个 app 有自己的 urls.py
- 主路由手动导入
- 模块化
- 不够优雅(需要手动拼接)
每个app 单独urls.py

然后导入到项目的urls.py
urlpatterns += shop_urlpatterns
urlpatterns += user_urlpatterns

8.7.4include+列表
特点:
- 使用 include() 分发
- 可以直接 include 一个列表
- 解耦
- 实际开发中较少这样写
urlpatterns = [
path("shop/", include(shop_urlpatterns)),
path("user/", include(user_urlpatterns)),
]

使用 include 可以将各个模块的 urls.py 进行统一分发管理。
主路由中定义前缀(如 user/、shop/),子路由中无需重复书写,include 会自动拼接完整访问路径。

8.7.5每个app 独立urls.py
特点:
- 每个 app 一个 urls.py
- 每个文件必须有 urlpatterns
- 主路由只负责分发
- 最清晰
- 最解耦
- 企业级标准写法
配置shop/urls.py和user/urls.py的路由,这里的路由名数组名称不能随便起了,必须是urlpatterns

然后在项目/urls.py进行导入
urlpatterns = [
# 方案五:每个 app 独立 urls.py(官方推荐 ⭐)
path("shop/", include("shop.urls")),
path("user/", include("user.urls")),
]

8.8应用名称空间
8.8.1普通路由分发冲突示例
【背景问题】
多个 app 下存在同名视图函数或路由别名时:
- 前端
- 后端 reverse('index_view')
都会出现冲突,默认解析到最后 include 的 app。
例如:
- app01.index 和 app02.index 都叫 index_view
- 主 urls.py 分发顺序:
path("app01/", include("app01.urls")),
path("app02/", include("app02.urls")), - 结果 {% url 'index_view' %} 会解析到 app02.index_view
首先创建2个app
startapp app01
startapp app02

创建app01的视图和注册路由
def index(request):
print(f"from app01 view : {reverse('index_view')}")
return HttpResponse("app01 index")
urlpatterns = [
path("index/", index, name="index_view")
]

创建app02的视图和注册路由
def index(request):
print(f"from app02 view : {reverse('index_view')}")
return HttpResponse("app02 index")
urlpatterns = [
path("index/", index, name="index_view")
]

在user/templates/index.html定义模版
<div>
<p>
<a href="/app01/index/">app01 的 index </a>
<br>
<a href="{% url 'index_view' %}">app01 的 index (路由解析)</a>
</p>
<p>
<a href="/app02/index/">app02 的 index </a>
<br>
<a href="{% url 'index_view' %}">app02 的 index (路由解析)</a>
</p>
</div>

如果直接指定模块路径访问index(例如:/app01/index/、/app02/index/)可以访问到对应app的index路由
但是通过路由解析2个路径只能解析到 app02

8.8.2app_name
定义:app_name 是 Django 中给一个 app 的 URL 模块 设定命名空间的标识符。
在app01/urls.py使用app_name声明名称空间
app_name = "app01"
urlpatterns = [
path("index/", index, name="index_view")
]
并且reverse时指定名称空间
def index(request):
print(f"from app01 view : {reverse('app01:index_view')}")
return HttpResponse("app01 index")

在app02/urls.py使用app_name声明名称空间
app_name = "app02"
urlpatterns = [
path("index/", index, name="index_view")
]
并且reverse时指定名称空间
def index(request):
print(f"from app02 view : {reverse('app02:index_view')}")
return HttpResponse("app02 index")

定义模版时也指定名称空间
<div>
<p>
<a href="/app01/index/">app01 的 index </a>
<br>
{# <a href="{% url 'index_view' %}">app01 的 index (路由解析)</a>#}
<a href="{% url 'app01:index_view' %}">app01 的 index (路由解析)</a>
</p>
<p>
<a href="/app02/index/">app02 的 index </a>
<br>
{# <a href="{% url 'index_view' %}">app02 的 index (路由解析)</a>#}
<a href="{% url 'app02:index_view' %}">app02 的 index (路由解析)</a>
</p>
</div>

这样就可以不同模块同名的视图了

8.8.3include时指定namespace
定义:namespace 是 Django 用来给 URL 路由 分组命名 的标识符。
作用:在反向解析时,保证 不同 app 的路由同名不会冲突。
形式:通常配合 include() 使用。
说明:
-
arg=("app01.urls", "app01") : 指定 urls 模块和 app_name
-
namespace="app01" : 分配给当前分发的 namespace
-
模板和 reverse 仍然使用 app_name:别名 的方式调用
{% url 'app01:index_view' %} / reverse("app01:index_view")
app_name 和 namespace
| 概念 | 定义 | 位置 | 作用 |
|---|---|---|---|
app_name |
给 子 app 的 urls.py 定义命名空间标识 | 子 app 的 urls.py 文件里 |
告诉 Django 这个 app 的内部路由属于哪个逻辑组;配合 namespace 使用 |
namespace |
给 include() 导入到主路由时 指定全局命名空间 | 主路由 urls.py 的 include() 调用 |
在全局范围内使用 {% url 'namespace:name' %} 或 reverse("namespace:name") 时前缀使用 |
app_name = 声明模块自己是谁;namespace = 调用路由时叫什么。
分发时以 namespace 为主,app_name 只是内部验证。

前端模版还是和以前一样,这里的app01:index_view里的app01指的是namespace
没有指定 namespace 时,模板里的 app01 就指向子 app urls.py 里的 app_name,Django 会隐式用它作为 namespace。

8.9路由转换器
8.9.1简介
Django 2.0+ 提供了 path() 的路由转换器功能,可以让 URL 路径中的参数直接被类型化,不用再写正则。
内置转换器类型
| 类型 | 说明 | 示例 |
|---|---|---|
str |
匹配非空字符串,不含 / |
<str:name> → "peng" |
int |
匹配整数 | <int:id> → 123 |
slug |
匹配字母、数字、下划线或短横线 | <slug:slug> → "my-first-post" |
uuid |
匹配 UUID | <uuid:uid> → "550e8400-e29b-41d4-a716-446655440000" |
path |
匹配非空字符串,可以包含 / |
<path:subpath> → "dir1/dir2/file" |
创建一个模块验证这些转换器
startapp order

8.9.2字符串转换器(str)
匹配规则:[^/]+
匹配除了 / 之外的任意非空字符串。
类型转换:匹配到的字符串保持原样(str 类型)。
适用场景:匹配一般文字、用户名、标题等。
示例:
# 路由
# 【1】 ● str
# ○ 匹配除了 '/' 之外的非空字符串。
# ○ 如果表达式内不包含转换器,则会默认匹配字符串。
# http://127.0.0.1:8000/order/str_pattern/peng/
path("str_pattern/<str:name>/", str_pattern, name="str_pattern"),
# 必须按照关键字接受参数 并且参数类型是字符串
# 匹配除了 '/' 之外的非空字符串 遇到 / 就会变成指定的路由去解析
# http://127.0.0.1:8000/order/str_pattern/peng/
# 先去这里找 : http://127.0.0.1:8000/order/str_pattern/dre/ 参数是 am
# 视图
def str_pattern(request, name, *args, **kwargs):
print(args) # ()
print(kwargs) # {'name': 'peng'}
print(name, type(name))
return HttpResponse("str_pattern")

8.9.3整数转换器(int)
匹配规则:[0-9]+
匹配一个或多个数字。
类型转换:匹配到的字符串自动转成整数(int)。
适用场景:ID、年份、页码等数字类型参数。
示例:
# 路由
# 【2】● int
# ○ 匹配 0 或任何正整数。返回一个 int 。
# http://127.0.0.1:8000/order/int_pattern/999/
path("int_pattern/<int:name>/", int_pattern, name="int_pattern"),
# 接受到的参数会被自动转换成 int 类型
# 视图
def int_pattern(request, name, *args, **kwargs):
print(args) # ()
print(kwargs) # {'name': 'peng'}
print(name, type(name))
return HttpResponse("int_pattern")

8.9.4短标签转换器(slug)
匹配规则:[-a-zA-Z0-9_]+
匹配字母、数字、下划线、短横线组合。
类型转换:返回字符串(str)。
适用场景:文章标题、URL-friendly 字符串。
示例:
# 【3】● slug
# ○ 匹配任意由 ASCII 字母或数字以及连字符和下划线组成的短标签。
# ○ 比如,building-your-1st-django-site 。
# http://127.0.0.1:8000/order/slug_pattern/peng_18/
path("slug_pattern/<slug:name>/", slug_pattern, name="slug_pattern"),
def slug_pattern(request, name, *args, **kwargs):
print(args) # ()
print(kwargs) # {'name': 'peng'}
print(name, type(name))
return HttpResponse("slug_pattern")

8.9.5UUID转换器(uuid)
匹配规则:[0-9a-fA-F-]{36}
匹配标准 UUID 格式,例如 550e8400-e29b-41d4-a716-446655440000。
类型转换:返回 UUID 对象(来自 uuid 模块)。
适用场景:资源唯一标识符(如用户 ID、文件 ID 等)。
示例:
# 【4】● uuid
# ○ 匹配一个格式化的 UUID 。为了防止多个 URL 映射到同一个页面,必须包含破折号并且字符都为小写。
# ○ 比如,3f8a2c1e-91b7-4f6d-8c2a-5e9d7b3c4a11。返回一个 UUID 实例。
# http://127.0.0.1:8000/order/uuid_pattern/3f8a2c1e-91b7-4f6d-8c2a-5e9d7b3c4a11/
path("uuid_pattern/<uuid:name>/", uuid_pattern, name="uuid_pattern"),
def uuid_pattern(request, name, *args, **kwargs):
print(args) # ()
print(kwargs) # {'name': 'peng'}
print(name, type(name))
return HttpResponse("uuid_pattern")

8.9.6路径转换器(path)
匹配规则:. +
匹配任意字符串,包括 /。
类型转换:返回字符串(str)。
适用场景:多层路径、文件路径、嵌套资源路径。
示例:
# 【5】● path
# ○ 匹配非空字段,包括路径分隔符 '/' 。
# ○ 它允许你匹配完整的 URL 路径而不是像 str 那样匹配 URL 的一部分。
# http://127.0.0.1:8000/order/path_pattern/peng/18/man/
path("path_pattern/<path:name>/", path_pattern, name="path_pattern"),
def path_pattern(request, name, *args, **kwargs):
print(args) # ()
print(kwargs) # {'name': 'peng'}
print(name, type(name))
return HttpResponse("path_pattern")

8.9.7自定义路径转换器
Django 的 path() 函数允许通过 注册转换器(register_converter)把 URL 参数从字符串自动转换为你需要的 Python 对象。
自定义转换器主要由以下三部分组成:
| 属性/方法 | 作用 |
|---|---|
regex(必需) |
一个正则表达式字符串,用于匹配 URL 中的参数。注意不能使用捕获组。 |
to_python(self, value)(必需) |
URL 匹配到的字符串如何转换成 Python 对象,例如 int、float、UUID、自定义类实例等。 |
to_url(self, value)(可选) |
反向解析时如何把 Python 对象转成 URL 字符串。如果不写,默认使用 str(value)。 |
自定义转换器
# (1)创建自定义路径转换器类
class FourDigitYearConverter:
"""
自定义转换器类,用于匹配四位数字年份。
核心功能:
1️⃣ regex: 定义匹配规则(必须是正则表达式字符串)
2️⃣ to_python: 将匹配到的字符串转换成 Python 数据类型
3️⃣ to_url: 反向生成 URL 时的格式化规则
"""
# 【正则匹配规则】regex
# 用于告诉 Django URL dispatcher 这个转换器可以匹配什么
# r"[0-9]{4}" 表示:匹配任意四位数字
# 示例:'2023'、'1999' 可以匹配,'23'、'abcd' 不匹配
regex = r"[0-9]{4}"
# 【URL → Python】转换函数
def to_python(self, value):
"""
Django 在解析 URL 时,会把匹配到的字符串调用这个函数。
参数:
value (str):URL 中匹配到的字符串,例如 '2026'
返回:
int:把字符串转换为整数类型,方便在视图中使用
"""
return int(value) # '2026' -> 2026
# 【Python → URL】反向生成函数
def to_url(self, value):
"""
Django 在使用 reverse() 或 {% url %} 生成 URL 时会调用。
参数:
value (int):传入的 Python 数据,例如 7、2026
返回:
str:格式化为四位数字字符串
- 如果 value 不够 4 位,会自动补 0
- 示例:7 -> '0007',2026 -> '2026'
"""
return "%04d" % value # 格式化为四位数字字符串

注册转换器
# 【一】导入自定义的路径转换器类
from order.path_converters import FourDigitYearConverter
# 【二】借助Django的转换器语法转换成 Django的转换器
from django.urls import register_converter
# 【三】注册你的转换器
register_converter(FourDigitYearConverter, "aaa")
urlpatterns = [
# 【0】自定义路径转换器
path("self_pattern/<aaa:name>/", self_pattern, name="self_pattern")
]

9.虚拟环境
9.1简介
虚拟环境介绍
- 虚拟环境是根据现有 Python 环境创建的隔离环境。
- 可以看作是项目的独立 Python 沙箱,避免全局环境冲突。
虚拟环境的应用场景
- 项目一:Django 3.x 开发
- 项目二:Django 5.x 开发
- 同一个解释器下无法同时存在两个版本,虚拟环境解决了环境隔离问题。
Python 的虚拟环境(virtual environment)用于为不同项目隔离依赖包,避免全局 Python 环境污染,保证项目可控性。常用创建方式有三种:
- Python 自带
venv - 第三方
virtualenv - IDE(PyCharm)集成方式。
我本身使用的是Miniconda,这里做学习了解
9.2venv
原理: venv 是 Python 3.3+ 自带的标准库模块,可以创建独立的 Python 环境。每个环境有独立的 python 可执行文件和 site-packages 目录。
优点:
- Python 自带,无需额外安装
- 与 Python 版本紧密绑定,创建快速
- 跨平台可用
注意事项:
venv只适用于 Python 3.x- 不会自动安装
pip(新版本 Python 会自带)
创建命令:
bin/或Scripts/(Windows): python 可执行文件lib/或Lib/:第三方库目录pyvenv.cfg:虚拟环境配置文件
# 在当前目录创建名为 myenv 的虚拟环境
python -m venv myenv
激活虚拟环境:
- Windows:
myenv\Scripts\activate
- macOS / Linux:
source myenv/bin/activate
退出虚拟环境:
deactivate

9.3virtualenv
原理:
virtualenv 是 Python 2 和 3 都可用的虚拟环境工具,比 venv 功能稍丰富,如支持旧版本 Python。
优点:
- 支持 Python 2 和 3
- 可以指定不同版本 Python 创建环境
- 功能更丰富(有些老项目依赖
virtualenvwrapper管理环境)
缺点:
- 需额外安装
virtualenv - Python 3.3+ 已自带
venv,通常没必要使用
安装:
pip install virtualenv
创建虚拟环境:
virtualenv myenv111
或者指定 Python 版本:
# 示例
# virtualenv -p /usr/bin/python3.10 myenv
virtualenv -p 路径/python3.10 myenv
激活虚拟环境:
- Windows:
myenv111\Scripts\activate
- macOS / Linux:
source myenv/bin/activate
退出虚拟环境:
deactivate

9.4PyCharm创建虚拟环境
文件 ---> 设置 ---> 项目:项目名称 ---> Python解释器 ---> 添加解释器 ---> 添加本地解释器

这里可以选择python环境和位置

点击确定就创建成功了

9.5项目共享与依赖管理
- 问题:将项目发给别人,别人可能不知道已安装哪些模块。
- 解决方案:导出依赖列表。
pip list

导出依赖文件 requirements.txt
pip freeze > requirements.txt

安装依赖
pip install -r requirements.txt
10.视图层
10.1Django三大响应方式
HttpResponse(原始响应)
- 用途:返回原始数据(字符串、HTML、JSON 等)
- 优缺点:
- 灵活,可返回任意数据
- 需要手动设置
content_type,JSON 需手动序列化
JsonResponse(专用 JSON 响应,推荐,这个也属于HttpResponse)
- 自动 JSON 序列化
- 自动设置
Content-Type: application/json - 推荐用于接口返回 JSON
render(模板渲染)
- 用途:返回 HTML 页面(内部 = HttpResponse + 模板渲染)
- 原理:
- 渲染模板 + 填充上下文 → 返回 HttpResponse
- 浏览器 不会发起新请求
- 地址栏保持不变
redirect(重定向)
- 用途:返回 302 重定向,让浏览器重新发起请求
- 特点:
- 浏览器收到 302 → 发起新请求
- 地址栏会改变
- 使用场景:登录跳转、表单提交后跳转
render / redirect / HttpResponse 核心对比
| 响应方式 | 类型 | 浏览器行为 | 地址栏 | 典型场景 |
|---|---|---|---|---|
HttpResponse |
原始数据 | 无 | 不变 | 自定义响应、调试接口 |
JsonResponse |
JSON | 无 | 不变 | 返回接口数据,推荐 |
render |
HTML 页面 | 无 | 不变 | 页面渲染显示 |
redirect |
302 重定向 | 会发起新请求 | 改变 | 登录跳转、表单提交跳转 |
render / redirect / HttpResponse之前学过了,这里不细讲了
10.2JsonResponse
本质:JsonResponse = HttpResponse + json.dumps + content_type封装
HttpResponse 返回 JSON 的问题
- 问题1:不能直接返回 dict
- 问题2:需要手动序列化
- 问题3:中文乱码
JsonResponse 核心优势
- JSON序列化
- 设置
Content-Type: application/json - 使用 DjangoJSONEncoder(支持 datetime 等)
源码解析
class JsonResponse(HttpResponse):
def __init__(self, data, encoder=DjangoJSONEncoder,
safe=True, json_dumps_params=None, **kwargs):
# 1️⃣ 安全限制(默认只允许 dict)
if safe and not isinstance(data, dict):
raise TypeError("必须是 dict,除非 safe=False")
# 2️⃣ 默认参数处理
if json_dumps_params is None:
json_dumps_params = {}
# 3️⃣ 默认 content_type
kwargs.setdefault('content_type', 'application/json')
# 4️⃣ JSON 序列化
data = json.dumps(data, cls=encoder, **json_dumps_params)
# 5️⃣ 调用 HttpResponse
super().__init__(content=data, **kwargs)

示例
def json_response(request):
info_data = {"name": "peng大哥", "age": "18"}
# 1.HttpResponse 返回 json 数据
# 中文会被转换成 ASCII {"name": "peng\u5927\u54e5", "age": "18"}
# info_data_str = json.dumps(info_data)
# ensure_ascii=False 控制 JSON 是否把非 ASCII 字符(比如中文)转义成 Unicode
# info_data_str = json.dumps(info_data,ensure_ascii=False)
# return HttpResponse(info_data_str, content_type="application/json")
# 2.JsonResponse 返回 JSON 格式数据
# 直接渲染也会把中文转码 ASCII
# {"name": "peng\u5927\u54e5", "age": "18"}
# return JsonResponse(info_data)
# 通过 json_dumps_params 配置 ensure_ascii
return JsonResponse(info_data, json_dumps_params={"ensure_ascii": False})

10.3request对象
request 本质:request 是 Django 封装的 HTTP 请求对象(HttpRequest)
作用:封装客户端请求的所有数据(请求头、参数、body、cookie 等)
| 参数 | 类型 | 数据来源 | 说明 |
|---|---|---|---|
request.GET |
QueryDict | URL 参数 | 获取 GET 请求参数 |
request.POST |
QueryDict | 请求体(表单) | 获取 POST 表单数据 |
request.FILES |
MultiValueDict | 上传文件 | 获取上传文件 |
request.body |
bytes | 原始请求体 | 获取 JSON / 原始数据 |
request.META |
dict | 请求头 + 环境变量 | 获取底层请求信息 |
request.headers |
dict-like | 请求头 | 更直观获取请求头 |
request.COOKIES |
dict | 浏览器 | 获取 cookie |
request.session |
SessionBase | 服务器 | 会话数据 |
request.user |
User对象 | 认证系统 | 当前登录用户 |
request.method |
str | HTTP协议 | 请求方式(GET/POST) |
request.path |
str | URL | 路径(不带参数) |
request.get_full_path() |
str | URL | 路径 + 参数 |
request.path_info |
str | URL | 类似 path(很少用) |
request.get_full_path_info() |
str | URL | 几乎不用 |
示例
def request_methods(request):
"""
Django HttpRequest 对象详解(常用属性全覆盖)
"""
# ======================================================
# 一、请求方式
# ======================================================
method = request.method # 请求方法(GET / POST / PUT / DELETE 等)
# ======================================================
# 二、请求参数(最核心 ⭐)
# ======================================================
get_data = request.GET # GET 请求参数(QueryDict)
post_data = request.POST # POST 表单数据(QueryDict)
files_data = request.FILES # 上传文件数据(MultiValueDict)
# ⚠️ 注意:
# request.GET / POST 都是 QueryDict(支持 getlist)
username = request.GET.get("username") # 获取单个参数
hobby_list = request.POST.getlist("hobby") # 获取多个值(复选框)
# ======================================================
# 三、原始请求体(API开发常用)
# ======================================================
body_data = request.body # 原始请求体(二进制)
# 一般用于 JSON:
# import json
# json.loads(request.body)
# ======================================================
# 四、路径相关
# ======================================================
path = request.path # 当前路径(不带参数)
path_info = request.path_info # 同 path
full_path = request.get_full_path() # 完整路径(带参数)
full_path_info = request.get_full_path_info() # 同上
# 示例:
# /user/test/?id=1
# path → /user/test/
# full_path → /user/test/?id=1
# ======================================================
# 五、请求头 & 环境变量
# ======================================================
meta = request.META # 原始请求头 + 环境变量(字典)
headers = request.headers # 更友好的请求头对象(推荐使用)
user_agent = request.headers.get("User-Agent") # 浏览器信息
host = request.get_host() # 主机名(127.0.0.1:8000)
port = request.get_port() # 端口号
# ======================================================
# 六、Cookie / Session / 用户
# ======================================================
cookies = request.COOKIES # 客户端 Cookie(字典)
session = request.session # session 数据(可存取)
user = request.user # 当前登录用户对象
# ======================================================
# 七、URL & URI 相关
# ======================================================
absolute_url = request.build_absolute_uri() # 完整 URL(含域名)
# ======================================================
# 八、安全相关
# ======================================================
is_secure = request.is_secure() # 是否 HTTPS 请求(True / False)
# ======================================================
# 九、底层读取方法(一般不用)
# ======================================================
# request.read() # 读取请求体
# request.readline() # 读取一行
# request.readlines() # 读取多行
# ======================================================
# 十、调试输出(建议开发时使用)
# ======================================================
print(f"""
================= 请求信息 =================
【请求方式】method: {method}
【GET参数】{get_data}
【POST参数】{post_data}
【FILES文件】{files_data}
【body原始数据】{body_data}
【路径】path: {path}
【完整路径】full_path: {full_path}
【请求头 headers】{headers}
【User-Agent】{user_agent}
【Host】{host}
【Port】{port}
【Cookies】{cookies}
【Session】{session}
【User】{user}
【完整URL】{absolute_url}
【是否HTTPS】{is_secure}
===========================================
""")
return render(request, "request_methods.html", locals())

10.4表单提交
注意:
application/x-www-form-urlencoded: 普通表单,数据 URL 编码,Django 获取方式 request.POST,不能上传文件multipart/form-data: 件上传,分块 multipart,Django 获取方式:request.POST + request.FILESrequest.FILES.get("avatar"): 获取上传文件
views.py
def register(request):
"""
用户注册示例视图函数
--------------------------
enctype 类型说明:
application/x-www-form-urlencoded → 普通表单,数据 URL 编码,Django 获取方式:request.POST
multipart/form-data → 文件上传,分块 multipart,Django 获取方式:request.POST + request.FILES
"""
# -------------------------
# 处理 GET 请求:返回注册页面
# -------------------------
if request.method == "GET":
# locals() 将当前函数局部变量传给模板
# 这里没有局部变量,也可以直接传空字典 {}
return render(request, "register.html", locals())
# -------------------------
# 处理 POST 请求:提交表单数据
# -------------------------
else:
# 普通文本数据从 request.POST 获取
username = request.POST.get("username") # 获取单个文本字段 username
password = request.POST.get("password") # 获取单个文本字段 password
avatar = request.POST.get("avatar") # 普通表单中 input type="text" 或 input type="file" 的值(文件名字符串)
hobby = request.POST.getlist("hobby") # 获取复选框 name="hobby" 的所有选中值,返回列表
gender = request.POST.get("gender") # 获取单选框或下拉框值
# 打印调试信息,查看数据类型和内容
print(f"""
"username: {username}"
"password: {password}"
"avatar: {avatar, type(avatar)}" # 注意:这里 avatar 是字符串(文件名),不是文件对象
"hobby: {hobby}"
"gender: {gender}"
""")
# 文件上传:真正的文件对象从 request.FILES 获取
file_obj = request.FILES.get("avatar") # avatar 对应 input type="file"
# 调试查看文件对象类型
print("file_obj", type(file_obj)) # <class 'django.core.files.uploadedfile.InMemoryUploadedFile'>
# 读取文件内容
file_data = file_obj.read() # 二进制数据
# 构建保存路径
# base_dir = 项目根目录
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
file_path = os.path.join(base_dir, "static", "avatar") # static/avatar 目录
# 如果目录不存在就创建
os.makedirs(file_path, exist_ok=True)
# 保存文件到磁盘
with open(os.path.join(file_path, file_obj.name), 'wb') as fp:
fp.write(file_data) # 写入二进制数据
# 表单处理完成后,重定向到注册页面(POST/Redirect/GET 规范)
return redirect(reverse("register"))
前端
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="{% static 'js/jquery.min.js' %}"></script>
<link href="{% static 'bootstrap/bootstrap.min.css' %}" rel="stylesheet">
<script src="{% static 'bootstrap/bootstrap.min.js' %}"></script>
</head>
<body>
{#创建容器#}
<div class="container-fluid">
{# 栅格系统 #}
<div class="row">
<div class="col-md-8 col-md-offset-2">
{# <form action="" method="post" enctype="application/x-www-form-urlencoded">#}
<form action="" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="InputUsername">Username</label>
<input type="text" name="username" class="form-control" id="InputUsername" placeholder="Username"
value="peng">
</div>
<div class="form-group">
<label for="InputPassword">Password</label>
<input type="password" name="password" class="form-control" id="InputPassword"
placeholder="Password" value="123qwe">
</div>
<div class="form-group">
<label for="InputFile">Avatar</label>
<input type="file" id="InputFile" name="avatar">
<p class="help-block">点击上传文件 上传头像</p>
</div>
<div class="form-group">
<span><b>爱好</b></span>
<br>
<label class="checkbox-inline">
<input type="checkbox" id="inlineCheckbox1" name="hobby" value="music" checked="true"> music
</label>
<label class="checkbox-inline">
<input type="checkbox" id="inlineCheckbox2" name="hobby" value="rap" checked="true"> rap
</label>
<label class="checkbox-inline">
<input type="checkbox" id="inlineCheckbox3" name="hobby" value="basketball" checked="true">
basketball
</label>
</div>
<div class="form-group">
<span><b>性别</b></span>
<br>
<label class="radio-inline">
<input type="radio" name="gender" id="inlineRadio1" value="male" checked="checked"> 男
</label>
<label class="radio-inline">
<input type="radio" name="gender" id="inlineRadio2" value="female"> 女
</label>
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
</div>
</div>
</body>
</html>
10.5CBV和FBV
FBV(函数视图)
- 一个 URL → 一个函数
- 通过
if request.method判断请求方式 - 简单直观,适合小项目
- 本质:函数 + 条件判断
CBV(类视图)
-
一个 URL → 一个类
-
不同请求方式 → 不同方法(get / post)
-
必须使用
.as_view() -
本质: 类 + 反射 + 请求分发
CBV 核心流程(必须会)
请求 → as_view() → dispatch()
↓
request.method → getattr()
↓
调用 get / post 方法
10.6CBV核心源码
整体调用链
URL
↓
as_view()
↓
view(request)
↓
实例化类(self = View())
↓
setup()(绑定 request)
↓
dispatch()(请求分发)
↓
get / post
↓
HttpResponse
三大核心组件
| 组件 | 作用 | 本质 |
|---|---|---|
as_view() |
类 → 函数 | 入口转换 |
setup() |
初始化 | 绑定 request |
dispatch() |
分发请求 | 核心调度器 |
dispatch 核心逻辑
本质:反射调用方法(get / post)
method = request.method.lower()
handler = getattr(self, method, self.http_method_not_allowed)
return handler(request)
关键机制拆解
1.as_view()
返回一个函数(闭包)
不是直接执行类
2.setup()
self.request = request
给类绑定 request / args / kwargs
补充 head → get
3.dispatch()
获取请求方式
判断是否合法
反射调用方法
4.getattr
动态方法调用
替代 if-else
5.405 机制
http_method_not_allowed
返回
HttpResponseNotAllowed
控制请求方式
超出范围 → 405
http_method_names = ["get", "post"]
核心本质
CBV = 函数包装类 + 反射 + 分发机制
Django 的 CBV 本质是通过 as_view() 将类转换为函数,在函数内部实例化类对象,然后通过 setup 绑定 request,再由 dispatch 根据 request.method 使用 getattr 反射调用对应的处理方法(如 get/post)。如果请求方法不被允许,则返回 405。
CBV = as_view(入口) + dispatch(分发) + getattr(执行)
11.模版层
11.1模版层渲染
11.1.2模版语法基础
逻辑标签
for: 循环遍历列表、字典等可迭代对象if: 条件判断block: 定义可被子模板覆盖的区域(模板继承)extends: 模板继承,用于复用父模板include: 引入其他模板片段,实现组件化- 加载系统:
{% load static %}
{% %}
变量渲染: 渲染后端传入的数据
{{ }}
11.1.2变量取值规则
在 Django 模板中,{{ obj.name }} 的点 . 并不是普通的 Python 属性访问,而是 “智能解析机制”。
当模板遇到 {{ obj.name }} 时,会按照下面顺序解析:
- 字典 key: 如果
obj是字典,首先尝试obj["name"] - 对象属性: 如果
obj是对象且存在属性name,返回obj.name - 方法调用(无参方法): 如果
obj.name是一个可调用方法(无参数),模板会自动调用它并返回结果
示例
{{ obj.name }}
等价逻辑(伪代码)
if obj["name"]:
return obj["name"]
elif hasattr(obj, "name"):
return obj.name
elif callable(obj.name):
return obj.name()
11.1.3数据类型渲染
| 类型 | 模板访问方式 | 注意事项 |
|---|---|---|
| int/float | {{ var }} |
float 自动去掉多余 0,可用 floatformat 固定格式 |
| str | {{ var.0 }} 或方法 |
不支持 Python 下标 [0] |
| list/tuple | {{ var.0 }} |
同上,索引从 0 开始 |
| dict | {{ var.key }} |
key 不存在返回空字符串 |
| bool | {{ var }} |
可结合 if 条件渲染 |
| set | {{ var }} |
无序,不能使用索引,建议先转 list |
示例
class IndexView(View):
# 八大基本数据类型的渲染
def get(self, request, *args, **kwargs):
# 整型
age = 18
# 浮点型
salary = 1000.4546464564
# 字符串
name = "My name is peng!"
# 列表
num_list = [1, 2, 3, "peng", "hope", "opp"]
# 字典
info_data = {"name": "peng", "age": 18}
# 布尔
is_male = True
# 集合
num_set = {1, 2, 3, "peng"}
# 元组
num_tuple = (1, 2, 3, "peng")
# 方法
def run(name, age):
print(666)
return 999
# 类
class Student:
def run(self):
return "self run"
@classmethod
def listen(cls):
return "cls listen"
@staticmethod
def work():
return "work"
def __str__(self):
return "__str__"
return render(request, "index.html", locals())
前端
{% load static %}
<!-- 加载静态文件标签库(用于解析 static 路径) -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- 引入 jQuery -->
<script src="{% static 'js/jquery.min.js' %}"></script>
<!-- 引入 Bootstrap CSS -->
<link href="{% static 'bootstrap/bootstrap.min.css' %}" rel="stylesheet">
<!-- 引入 Bootstrap JS -->
<script src="{% static 'bootstrap/bootstrap.min.js' %}"></script>
</head>
<body>
<h1>渲染八大基本数据类型</h1>
<!-- ================= 整数 ================= -->
<h2>渲染整数类型</h2>
<p>
{{ age }}
<!-- 直接输出变量 -->
</p>
<!-- ================= 浮点数 ================= -->
<h2>渲染浮点数类型</h2>
<p>
{{ salary }}
<!-- Django 会自动转为字符串输出 -->
</p>
<!-- ================= 字符串 ================= -->
<h2>渲染字符串类型</h2>
<p>
{{ name }}
<!-- 直接输出字符串 -->
<!-- ❌ Django 不支持 Python 下标写法 -->
{# {{ name[0] }} #}
<br>
{{ name.0 }}
<!-- ✅ Django 用 .索引 访问字符串中的字符 -->
<br>
{{ name.title }}
<!-- 每个单词首字母大写(调用字符串方法) -->
<br>
{{ name.capitalize }}
<!-- 仅第一个单词首字母大写 -->
</p>
<!-- ================= 列表 ================= -->
<h2>渲染列表类型</h2>
<p>
{{ num_list }}
<!-- 输出整个列表 -->
{{ num_list.0 }}
<!-- 访问第一个元素(Django统一用 .) -->
</p>
<!-- ================= 字典 ================= -->
<h2>渲染字典类型</h2>
<p>
{{ info_data }}
<!-- 输出整个字典 -->
<br>
info_data 0 {{ info_data.0 }}
<!-- ⚠️ 如果 key 是数字字符串才可以这样访问 -->
<br>
{{ info_data.keys }}
<!-- 获取所有 key(类似 dict.keys()) -->
{{ info_data.values }}
<!-- 获取所有 value -->
{{ info_data.name }}
<!-- 访问 key = 'name' 的值 -->
</p>
<!-- ================= 布尔 ================= -->
<h2>渲染布尔类型</h2>
<p>
{{ is_male }}
<!-- True / False 会直接输出 -->
</p>
<!-- ================= 元组 ================= -->
<h2>渲染元组类型</h2>
<p>
{{ num_tuple }}
<!-- 输出整个元组 -->
{{ num_tuple.0 }}
<!-- 访问第一个元素 -->
</p>
<!-- ================= 集合 ================= -->
<h2>渲染集合类型</h2>
<p>
{{ num_set }}
<!-- 输出集合(⚠️ 无序) -->
<br>
num_set : {{ num_set.0 }}
<!-- ❌ 不推荐:集合是无序的,不能保证有索引 -->
</p>
<!-- ================= 函数 ================= -->
<h1>渲染函数</h1>
<p>
有参函数run : {{ run }}
</p>
<p>
无参函数go : {{ go }}
</p>
<!-- ⚠️ Django 不会自动执行函数,只会显示函数对象 -->
<!-- 如果想执行:需要在视图中调用后传入结果 -->
<!-- ================= 类 ================= -->
<h1>渲染类</h1>
<div>
<p>
Student :>>> {{ Student }}
<!-- 输出类本身 -->
</p>
<p>
对象的绑定方法 {{ Student.run }}
<!-- ⚠️ 不会执行,只是方法引用 -->
</p>
<p>
类的绑定方法 {{ Student.listen }}
<!-- 类方法 -->
</p>
<p>
静态方法 {{ Student.work }}
<!-- 静态方法 -->
</p>
</div>
<!-- ================= 类的对象 ================= -->
<h1>渲染类的对象</h1>
<div>
<p>
student :>>> {{ student }}
<!-- 输出对象(通常是 __str__ 方法结果) -->
</p>
<p>
对象的绑定方法 {{ student.run }}
<!-- ⚠️ 不会执行 -->
</p>
<p>
类的绑定方法 {{ student.listen }}
</p>
<p>
静态方法 {{ student.work }}
</p>
</div>
</body>
</html>

11.1.4函数渲染
在 Django 模板中,函数的渲染逻辑 与 Python 普通调用不同,需要注意以下规则:
-
有参函数 → 必须在视图里调用
-
模板只会调用“无参函数/方法”。模板中写
{{ func }},如果func是无参函数,它会被自动调用 -
函数有返回值 → 渲染返回值
-
函数无返回值(返回 None) → 渲染 None
示例代码见数据类型渲染

11.1.5类和对象的渲染
结论
- 类本身访问方法 → 模板会执行(只要是无参 callable,包括类方法和静态方法)
- 对象访问方法 → 模板不会执行(因为绑定了 self,模板无法提供)
模板只会自动执行“无参 callable”,类的方法是无参的,绑定到对象的方法是有 self 的 → 不执行
示例代码见数据类型渲染

11.2过滤器
11.2.1概念
作用:对后端传递给模板的数据进行 格式化处理,类似函数调用。
语法:
{{ value|filter_name:参数 }}
本质:过滤器就是对模板变量进行 函数处理,等价于:
{{ value|length }}
≈
length(value)
11.2.2常用内置过滤器
| 过滤器 | 用法示例 | 适用类型 | 说明 |
|---|---|---|---|
length |
`{{ value | length }}` | 字符串、列表、字典 |
default |
`{{ value | default:"默认值" }}` | 任意 |
filesizeformat |
`{{ value | filesizeformat }}` | 数字 |
date |
`{{ value | date:"Y-m-d H:i:s" }}` | datetime |
slice |
`{{ value | slice:"0:3" }}` | 字符串、列表 |
truncatechars |
`{{ value | truncatechars:5 }}` | 字符串 |
cut |
`{{ value | cut:"a" }}` | 字符串 |
join |
`{{ value | join:"-" }}` | 列表 |
add |
`{{ value | add:10 }}` | 数字 |
11.2.3安全渲染(防XSS攻击)
Django 默认 HTML 会被转义:
"<h1>hello</h1>" → <h1>hello</h1>\
方法一:模板前端 safe 过滤器
- 告诉 Django 这是安全的 HTML,不要转义
- 有安全风险(可能导致 XSS 攻击)
{{ value|safe }}
方法二:后端 mark_safe(推荐)
from django.utils.safestring import mark_safe
html_str = "<h1>标题</h1>"
html_safe = mark_safe(html_str)
return render(request, "index.html", {"html": html_safe})
11.2.4过滤器链式调用
过滤器可以 连续调用:
{{ value|filter1|filter2|filter3 }}
示例
先将 name 转大写,再求长度
{{ name|upper|length }}
11.2.5示例
views.py
class IndexView1(View):
def get(self, request, *args, **kwargs):
html_str = "<a href="">点我有惊喜</a>"
html_str_new = mark_safe("<a href="">点我有惊喜</a>")
is_male = ""
file_path = os.path.join(os.path.dirname(__file__), "admin.py")
file_obj = open(file_path, "r", encoding="utf-8").read()
now_time = datetime.datetime.now()
info_str = "My name is peng!"
inf_list = ["my", "name", "is", "peng", "!"]
number = "100"
return render(request, "index1.html", locals())
前端
{% load static %}
<!-- 加载静态资源标签库 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- 引入 jQuery -->
<script src="{% static 'js/jquery.min.js' %}"></script>
<!-- 引入 Bootstrap CSS -->
<link href="{% static 'bootstrap/bootstrap.min.css' %}" rel="stylesheet">
<!-- 引入 Bootstrap JS -->
<script src="{% static 'bootstrap/bootstrap.min.js' %}"></script>
</head>
<body>
<!-- ================= length ================= -->
<p>{{ html_str|length }}</p>
<!-- 获取字符串长度(也适用于 list / dict / tuple) -->
<!-- ================= default ================= -->
<p>{{ is_male|default:"默认值" }}</p>
<!-- 如果变量为空(None / 空字符串 / False),显示默认值 -->
<!-- ================= filesizeformat ================= -->
<p>{{ file_obj|filesizeformat }}</p>
<!-- 将文件大小格式化为人类可读格式 -->
<!-- 例如:1024 -> 1 KB -->
<!-- ================= 日期原始输出 ================= -->
<p>{{ now_time }}</p>
<!-- 直接输出 datetime 对象(默认格式,不友好) -->
<!-- ================= date ================= -->
<p>{{ now_time|date:"Y-m-d H:i:s" }}</p>
<!-- 格式化时间 -->
<!-- Y: 年 m: 月 d: 日 H: 时 i: 分 s: 秒 -->
<!-- ================= 普通字符串 ================= -->
<p>{{ info_str }}</p>
<!-- 原样输出字符串 -->
<!-- ================= slice ================= -->
<p>{{ info_str|slice:"0:5:2" }}</p>
<!-- 字符串切片:start:end:step -->
<!-- 类似 Python:info_str[0:5:2] -->
<!-- ================= truncatechars ================= -->
<p>{{ info_str|truncatechars:5 }}</p>
<!-- 截断字符串(保留5个字符,多余用...代替) -->
<!-- 总长度 5(包含省略号) -->
<!-- 实际字符数 4 + … -->
<!-- ================= cut ================= -->
<p>{{ info_str|cut:"e" }}</p>
<!-- 删除字符串中所有 "e" 字符 -->
<!-- ================= join ================= -->
<p>{{ inf_list|join:"-" }}</p>
<!-- 将列表用指定字符拼接 -->
<!-- 例如:["a","b","c"] -> a-b-c -->
<!-- ================= add ================= -->
<p>{{ number|add:"999" }}</p>
<!-- 数值加法(字符串也可以拼接) -->
<!-- ⚠️ 注意:参数是字符串类型 -->
<!-- ================= HTML 自动转义 ================= -->
<p>{{ html_str }}</p>
<!-- Django 默认开启自动转义 -->
<!-- HTML 标签会被转义成字符串(防止 XSS 攻击) -->
<!-- ================= safe ================= -->
<p>{{ html_str|safe }}</p>
<!-- 关闭转义,让 HTML 生效 -->
<!-- ⚠️ 有安全风险(可能导致 XSS 攻击) -->
<!-- ================= 后端已处理 HTML ================= -->
<p>{{ html_str_new }}</p>
<!-- 如果后端使用 mark_safe 处理过,这里会直接渲染 HTML -->
</body>
</html>

11.3循环
11.3.1for
作用:遍历可迭代对象(list / dict / queryset 等)
语法:
{% for item in data %}
{{ item }}
{% endfor %}
11.3.2forloop内置变量
forloop 提供循环状态信息,可在模板中使用:
| 变量 | 说明 |
|---|---|
forloop.counter |
正序索引,从 1 开始 |
forloop.counter0 |
正序索引,从 0 开始 |
forloop.revcounter |
倒序索引,从 1 开始 |
forloop.revcounter0 |
倒序索引,从 0 开始 |
forloop.first |
是否第一次循环(True/False) |
forloop.last |
是否最后一次循环(True/False) |
forloop.parentloop |
外层循环对象(用于嵌套循环) |
示例:
{% for item in data %}
{{ forloop.counter }} - {{ item }}
{% endfor %}
11.3.3for-empty
当可迭代对象为空时,执行 {% empty %} 块:
{% for item in data %}
{{ item }}
{% empty %}
暂无数据
{% endfor %}
11.3.4if
模板中 if 支持标准条件判断:
{% if 条件 %}
...
{% elif 条件 %}
...
{% else %}
...
{% endif %}
11.3.5if支持的操作
比较运算
==, !=, >, <, >=, <=
逻辑运算
and, or, not
成员判断
in, not in
11.3.6嵌套循环
在嵌套循环中,可以通过 forloop.parentloop 访问外层循环信息:
{% for row in data %}
{% for col in row %}
外层索引:{{ forloop.parentloop.counter }}
内层索引:{{ forloop.counter }}
{% endfor %}
{% endfor %}
11.3.7示例
views.py
class IndexView2(View):
def get(self, request, *args, **kwargs):
info_data = {}
for i in range(50):
info_data[f"peng_{i}"] = {
"name": f"peng_{i}",
"age": i,
"gender": "male" if i % 2 == 0 else "female"
}
score = 99
return render(request, "index2.html", locals())
前端
{% load static %}
<!-- 加载静态文件标签库 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- 引入 jQuery -->
<script src="{% static 'js/jquery.min.js' %}"></script>
<!-- 引入 Bootstrap 样式 -->
<link href="{% static 'bootstrap/bootstrap.min.css' %}" rel="stylesheet">
<!-- 引入 Bootstrap JS -->
<script src="{% static 'bootstrap/bootstrap.min.js' %}"></script>
</head>
<body>
<table class="table">
<!-- Bootstrap 表格样式 -->
<thead>
<tr>
<th>#</th>
<th>姓名</th>
<th>年龄</th>
<th>性别</th>
</tr>
</thead>
<tbody>
<!-- ================= 循环开始 ================= -->
{% for info_datum in info_data.values %}
<!-- 遍历字典 info_data 的 value
info_data = {
"peng_0": {...},
"peng_1": {...}
}
info_data.values → 所有 value(字典对象)
-->
<!-- ===== 判断是否是第一条数据 ===== -->
{% if forloop.first %}
<!-- forloop.first = True(第一条) -->
<tr class="success">
<!-- 第一行用绿色高亮 -->
<th scope="row">{{ forloop.revcounter0 }}</th>
<!-- 反向索引(从 0 开始)
假设总共 50 条:
第一条 → 49
-->
<td>{{ info_datum.name }}</td>
<!-- 等价于 info_datum["name"] -->
<td>{{ info_datum.age }}</td>
<td>{{ info_datum.gender }}</td>
</tr>
<!-- ===== 判断是否是最后一条 ===== -->
{% elif forloop.last %}
<!-- forloop.last = True(最后一条) -->
<tr class="warning">
<!-- 最后一行黄色 -->
<th scope="row">{{ forloop.revcounter0 }}</th>
<td>{{ info_datum.name }}</td>
<td>{{ info_datum.age }}</td>
<td>{{ info_datum.gender }}</td>
</tr>
<!-- ===== 中间数据 ===== -->
{% else %}
<tr class="active">
<!-- 中间行默认样式 -->
<th scope="row">{{ forloop.revcounter0 }}</th>
<td>{{ info_datum.name }}</td>
<td>{{ info_datum.age }}</td>
<td>{{ info_datum.gender }}</td>
</tr>
{% endif %}
<!-- ================= 空数据处理 ================= -->
{% empty %}
<!-- 如果 info_data 为空(没有数据) -->
<tr class="active">
<th scope="row">1</th>
<td>1</td>
<td>1</td>
<td>1</td>
</tr>
{% endfor %}
<!-- ================= 循环结束 ================= -->
</tbody>
</table>
<hr>
</body>
</html>

11.4自定义模板标签inclusion_tag
11.4.1简介
使用场景
inclusion_tag 是 Django 自定义模板标签的一种,典型场景是:
- 多个页面使用同一块内容框架(比如广告位、导航栏、公共信息块)
- 每个页面的内容不同,但结构相同
- 你想在模板中“复用”一段 HTML,同时还能向它传递参数
普通函数 vs inclusion_tag:
- 普通函数:可以写逻辑,但在模板中无法直接传参渲染 HTML。
- inclusion_tag:可以在模板里调用函数并传参,同时返回一个模板片段渲染结果。
特点
- 模板复用: 通过 inclusion_tag,可以把重复的 HTML 和逻辑封装在一个模板片段里,多页面共享。
- 支持参数传递: 可以在模板中调用时传入变量,灵活控制渲染内容。
- 返回的是渲染后的模板: inclusion_tag 返回的是一个模板渲染后的 HTML,而不是字符串或原始数据。
11.4.2实现
11.4.2.1创建前端模版
这里以一个广告位为例,adv_temp_html.html需要传入info_data参数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>VIP 广告位</title>
<style>
/* 给广告卡片专属样式,不影响其他 .thumbnail */
.thumbnail.vip-ad {
width: 100px;
height: 180px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
flex-direction: column;
background-color: #fff;
}
.thumbnail.vip-ad:hover {
transform: translateY(-3px);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.2);
}
.thumbnail.vip-ad img {
width: 100%;
height: 80px;
object-fit: cover;
}
.thumbnail.vip-ad .caption {
padding: 10px;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.thumbnail.vip-ad .caption h3 {
font-size: 10px;
margin: 5px 0;
color: #007b5e;
}
.thumbnail.vip-ad .caption p {
font-size: 10px;
color: #555;
margin-bottom: 8px;
}
.thumbnail.vip-ad .caption p:last-child {
display: flex; /* 让按钮在一行 */
gap: 5px; /* 按钮之间间距 */
margin: 0; /* 去掉默认 p 的上下 margin */
}
.thumbnail.vip-ad .caption p a.vip-btn {
font-size: 10px;
padding: 5px 10px;
display: inline-block;
background-color: #007b5e !important; /* 强制覆盖 */
color: #fff !important;
text-align: center;
border-radius: 4px;
text-decoration: none;
}
.thumbnail.vip-ad .caption p a.vip-btn:hover {
background-color: #005a43 !important;
}
</style>
</head>
<body>
<h1>VIP 广告位</h1>
<div class="row">
{% for info_item in info_data %}
<div class="col-sm-6 col-md-4">
<!-- 仅加一个辅助类 vip-ad -->
<div class="thumbnail vip-ad">
<img src="{{ info_item.img_src }}" alt="{{ info_item.img_alt }}">
<div class="caption">
<h3>{{ info_item.title }}</h3>
<p>{{ info_item.desc }}</p>
<p>
<a href="{{ info_item.target_url }}" class="vip-btn">直达</a>
<a href="https://www.baidu.com" class="vip-btn">打我</a>
</p>
</div>
</div>
</div>
{% endfor %}
</div>
</body>
</html>

11.4.2.2编写Python文件逻辑
在你的 Django app 目录下创建 templatetags 文件夹,然后创建CommonInclusionTag.py
以 CommonInclusionTag.py 为例:
# 【一】导入模块
from django import template
# 【二】创建 register 对象
# 这句话就这么写 别改名
# Library 图书馆 ---> 创建了一个Django的图书馆对象
# 我们需要向当前的图书馆中增加我们自己定义的数据内容
register = template.Library()
# 【三】定义当前模版
# 【1】注册当前的模版 --- 参数写 自己的模版文件名(前端文件名)
# 前端文件写在 templates 里面 检索到 templates 里面的文件
@register.inclusion_tag("adv_temp_html.html")
def adv_temp(info_data):
# 将当前的局部名称空间返回
return locals()

11.4.2.3在页面模板中调用
调用的时候需要传入info_data参数
def index(request):
info_data = [
{
"title": "重金求子", "desc": "花重金求良子",
"img_src": "https://pic-image.yesky.com/uploadImages/newPic/2023/200/33/672KLME45A6B.png",
"img_alt": "图片走丢了 ~~ ",
"target_url": "https://www.bing.com/?mkt=zh-CN"
},
{
"title": "美女荷官", "desc": "在线发牌",
"img_src": "https://www.63099.net/zb_users/upload/2024/09/1725899463861.png",
"img_alt": "图片走丢了 ~~ ",
"target_url": "https://www.bing.com/?mkt=zh-CN"
},
]
return render(request, "index.html", locals())

前端导入CommonInclusionTag,然后调用传入info_data参数
{% load CommonInclusionTag %}
{% adv_temp info_data %}

总结
| 步骤 | 内容 |
|---|---|
| 创建包 | 在 app 下创建 templatetags 文件夹,添加 __init__.py |
| 写 Python | 定义函数并用 @register.inclusion_tag('模板路径') 装饰 |
| 返回值 | 函数返回字典,字典内容用于模板渲染 |
| 模板片段 | 在指定模板里使用传入字典渲染 HTML |
| 模板调用 | {% load 模块名 %} + {% 函数名 参数 %} |
11.5模版继承
11.5.1简介
模板继承是 Django 模板系统中的核心机制,它让你可以 定义基础模板,然后由子模板继承并修改其中的部分内容,实现页面结构复用。
使用场景
- 统一页面布局: 如网站的头部(导航栏)、底部(页脚)、侧边栏等固定区域。
- 多页面共享样式和结构
- 避免重复写 HTML 结构和样式
- 只需要在子模板里修改特定区域即可
- 典型应用: 首页、文章页、产品页等都使用同一套基础布局
基础概念
| 标签 | 用法 | 说明 |
|---|---|---|
{% block 块名 %}…{% endblock %} |
定义可被子模板覆盖的内容区域 | 块名必须唯一,用于标识可替换区域 |
{% extends "base.html" %} |
指定继承的父模板 | 子模板必须放在文件顶部,指定父模板路径 |
11.5.2实现
一般页面主要由导航条,页面内容,页脚组成
定义base.html,拆分了head、title、网站头部、导航条、页面内容、页脚
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}网站默认标题{% endblock %}</title>
{% block extra_head %}{% endblock %}
</head>
<body>
<header>
{% block header %}
<h1>网站头部</h1>
{% endblock %}
</header>
<nav>
{% block nav %}
<ul>
<li>首页</li>
<li>文章</li>
<li>关于</li>
</ul>
{% endblock %}
</nav>
<main>
{% block content %}
主内容区域
{% endblock %}
</main>
<footer>
{% block footer %}
页脚 © 2026
{% endblock %}
</footer>
</body>
</html>


定义子页面,使用extends 继承base.html,重写了title、页面内容、页脚,其他复用
{% extends "base.html" %}
{% block title %}文章页面{% endblock %}
{% block content %}
<h2>文章标题</h2>
<p>这里是文章内容</p>
{% endblock %}
{% block footer %}
<p>自定义页脚 © 2026</p>
{% endblock %}


12.模型层
12.1Django ORM配置
12.1.1配置Django环境
从manage.py复制Django环境代码:
os.environ.setdefault→ 设置 Django 配置模块django.setup()→ 初始化 ORM 和 app,必须在导入模型前执行
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo07.settings')
django.setup() # 临时启动 Django

12.1.2导入模型表
所有的模型表都在models.py里面,提前导入模型表
from user.models import User

12.1.3数据库配置
在settings.py中配置数据库连接
# 数据库配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', # 数据库引擎
'NAME': 'django_demo', # 数据库名
'USER': 'root', # 数据库用户名
'PASSWORD': 'root', # 数据库密码
'HOST': '192.168.188.180', # 数据库主机
'PORT': '3306', # 数据库端口
}
}

12.1.4MySQL补丁/安装模块
数据库连接配置好后启动项目可能会报错: Django想用 MySQL 驱动 MySQLdb,但你环境里没有
Watching for file changes with StatReloader
Exception in thread django-main-thread:
Traceback (most recent call last):
File "../miniconda\package\envs\django_env\Lib\site-packages\django\db\backends\mysql\base.py", line 16, in <module>
import MySQLdb as Database
ModuleNotFoundError: No module named 'MySQLdb'

第一种方式: 安装mysqlclient,官方推荐
直接安装mysqlclient,官方推荐,我使用的就是这种方式
pip install mysqlclient

第二种方式安装: PyMySQL
安装PyMySQL,我这里记录一下这种方式
pip install mysqlclient
项目的 __init__ 文件进行导入
import pymysql
pymysql.install_as_MySQLdb()

12.1.5创建模型表
这里以User为例
# 创建模型表
class User(models.Model):
name = models.CharField(max_length=32, verbose_name="姓名")
age = models.IntegerField(verbose_name="年龄")
# 注册时间 auto_now 第一注册会加 然后每一次更新数据 会随着更新
register_time = models.DateTimeField(verbose_name="注册时间", auto_now_add=True)
# 更新时间 auto_now_add 在第一次创建的时候自动加上当前时间 更新的时候不变
update_time = models.DateTimeField(verbose_name="更新时间", auto_now=True)
# 配置表元信息
class Meta:
# 表名
db_table = "user"
class BaseModel(models.Model):
# 注册时间 auto_now 在第一次创建的时候自动加上当前时间 更新的时候不变
create_time = models.DateTimeField(verbose_name="注册时间", auto_now_add=True)
# 更新时间 auto_now_add 第一注册会加 然后每一次更新数据 会随着更新
update_time = models.DateTimeField(verbose_name="更新", auto_now=True)
# 不生成表
class Meta:
abstract = True

12.1.6数据库迁移
执行数据库迁移命令的时候注意自己的虚拟环境
# 生成迁移记录
python manage.py makemigrations
# 执行迁移
python manage.py migrate

也可以使用manage.py执行,这样就不用输入python manage.py
# 生成迁移记录
makemigrations
# 执行迁移
migrate

12.1.7总结
if __name__ == '__main__':
# 1. 配置 Django 环境
# 可以直接从`manage.py`中拷贝
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo07.settings')
django.setup() # 临时启动 Django
# 2. 导入模型表
from user.models import User
from django.db import connection
# 【三】数据库配置(连接 MySQL)
'''
# 数据库配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', # 数据库引擎
'NAME': 'django_demo', # 数据库名
'USER': 'root', # 数据库用户名
'PASSWORD': 'root', # 数据库密码
'HOST': '192.168.188.180', # 数据库主机
'PORT': '3306', # 数据库端口
}
}
'''
# 【四】MySQL 补丁 / 安装模块
'''
import pymysql
pymysql.install_as_MySQLdb()
'''
# 【五】创建模型表示例
'''
from django.db import models
class User(models.Model):
name = models.CharField(max_length=32, verbose_name="姓名")
age = models.IntegerField(verbose_name="年龄")
update_time = models.DateTimeField(verbose_name="注册时间", auto_now=True) # 创建时自动加当前时间
register_time = models.DateTimeField(verbose_name="更新时间", auto_now_add=True) # 更新时自动更新
class Meta:
db_table = "user" # 表名
'''
# 【六】数据库迁移命令
# 1. 生成迁移记录:python manage.py makemigrations
# 2. 执行迁移:python manage.py migrate
"""
# 之前创建了很多表,可以把这个库清了,重新创建
DROP DATABASE django_demo;
CREATE DATABASE django_demo DEFAULT CHARSET utf8mb4;
"""
12.2Django ORM基本操作
12.2.1增加
# 1. 清空表并重置自增 ID
with connection.cursor() as cursor:
cursor.execute("TRUNCATE TABLE user;")
print_sep("表已清空,自增ID重置")
# 2. 新增
u1 = User.objects.create(name="peng_1", age=18)
u2 = User(name="peng_2", age=19)
u2.save()
u3 = User.objects.create(name="peng_3", age=20, register_time=datetime.datetime.now())
print_sep("新增用户完成", [u1, u2, u3])
12.2.2查询
# 3. 查询
qs = User.objects.filter(name__contains="peng")
print_sep("查询包含 'peng' 的记录", list(qs))
# 第一条
first_obj = qs.first()
print_sep("第一条记录", first_obj)
# 最后一条
last_obj = qs.last()
print_sep("最后一条记录", last_obj)
# get → 单对象
obj = User.objects.get(name="peng_1")
print_sep("get 查询的对象", obj)
# 全表查询
all_objs = User.objects.all()
print_sep("全表查询", list(all_objs))
12.2.3修改
# 4. 修改
User.objects.filter(name="peng_1").update(name="peng_111")
print_sep("批量更新完成,最新数据", list(User.objects.all()))
obj2 = User.objects.get(name="peng_2")
obj2.name = "peng_222"
obj2.save()
print_sep("对象更新完成,最新数据", list(User.objects.all()))
12.2.4字段提取
# 5. 字段提取
values_data = list(User.objects.values("name", "age"))
values_list_data = list(User.objects.values_list("name", "age"))
print_sep("values 提取字段", values_data)
print_sep("values_list 提取字段", values_list_data)
12.2.5去重
# 6. 去重
distinct_data = list(User.objects.values("name").distinct())
print_sep("去重 name", distinct_data)
12.2.6排序
# 7. 排序
print_sep("按 age 升序", list(User.objects.order_by("age")))
print_sep("按 age 降序", list(User.objects.order_by("-age")))
print_sep("按 age 逆序", list(User.objects.order_by("age").reverse()))
12.2.7统计
# 8. 统计
count = User.objects.count()
print_sep("表总条数", count)
12.2.8排除
# 9. 排除
exclude_data = list(User.objects.exclude(name="peng_3"))
print_sep("排除 name='peng_3'", exclude_data)
12.2.9判断存在
# 10. 判断存在
exists = User.objects.filter(name="peng_111").exists()
print_sep("是否存在 name='peng_111'", exists)
12.2.10查看 SQL
print_sep("查询 SQL", User.objects.all().query)
12.2.11总结
| 方法 | 功能说明 |
|---|---|
create() |
新增并保存对象 |
save() |
新增或修改(触发 pre_save / post_save 钩子) |
all() |
查询全部数据 |
filter() |
条件查询(返回 QuerySet,可链式操作) |
get() |
精确单条查询(唯一,不存在或多条会报错) |
update() |
批量更新(不触发 save() 钩子) |
delete() |
删除对象或 QuerySet 数据 |
values() |
返回字典形式字段列表(dict) |
values_list() |
返回元组形式字段列表(tuple) |
distinct() |
去重查询 |
order_by() |
排序(可升序/降序) |
count() |
统计记录总数 |
exclude() |
排除指定条件记录 |
exists() |
判断是否存在符合条件的记录(性能优于 len()) |
query |
查看 ORM 对应 SQL 语句 |

12.3Django ORM双下划线查询
12.3.1大小比较
# ======================================================
# 【一】大小比较
# ======================================================
# > gt, >= gte, < lt, <= lte
print("【gt】age>4")
print(User.objects.filter(age__gt=4).values("name", "age"))
print("【gte】age>=4")
print(User.objects.filter(age__gte=4).values("name", "age"))
print("【lt】age<4")
print(User.objects.filter(age__lt=4).values("name", "age"))
print("【lte】age<=4")
print(User.objects.filter(age__lte=4).values("name", "age"))
12.3.2in
# ======================================================
# 【二】或条件 / 多值匹配
# ======================================================
# in: age 属于 (0,4,16)
print("【in】age in (0,4,16,20)")
print(User.objects.filter(age__in=(0, 4, 16, 20)).values("name", "age"))
12.3.3范围查询
# ======================================================
# 【三】范围查询
# ======================================================
# range: SQL BETWEEN
print("【range】age between 0 and 18")
print(User.objects.filter(age__range=(0, 18)).values("name", "age"))
12.3.4模糊查询
# ======================================================
# 【四】模糊查询
# ======================================================
# contains: 区分大小写, icontains: 不区分大小写
print("【contains】name contains 'p' (区分大小写)")
print(User.objects.filter(name__contains="p").values("name", "age"))
print("【icontains】name icontains 'p' (不区分大小写)")
print(User.objects.filter(name__icontains="p").values("name", "age"))
12.3.5开头/结尾匹配
# ======================================================
# 【五】开头 / 结尾匹配
# ======================================================
print("【startswith】name starts with 'p'")
print(User.objects.filter(name__startswith="p").values("name", "age"))
print("【endswith】name ends with '2'")
print(User.objects.filter(name__endswith="2").values("name", "age"))
12.3.6日期查询
# ======================================================
# 【六】日期查询
# ======================================================
# 注意:register_time 是 DateTimeField
# 只查“注册日期是28号”的记录
print("【按日】register_time day=28")
print(User.objects.filter(register_time__day="28").values("name", "age"))
print("【按年】register_time year=2026")
# 只查“注册年份是2003”的记录
print(User.objects.filter(register_time__year="2026").values("name", "age"))
# RuntimeWarning: DateTimeField User.register_time received a naive datetime (2003-01-07 00:00:00) while time zone support is active. warnings.warn(
# print("【大于】register_time > 2003-01-07")
# print(User.objects.filter(register_time__gt="2003-01-07").values("name", "age"))
# print("【小于】register_time < 2003-01-07")
# print(User.objects.filter(register_time__lt="2003-01-07").values("name", "age"))
# 创建 naive datetime
naive_dt = datetime.datetime(2003, 1, 7)
# 转成带时区 datetime(aware datetime)
aware_dt = timezone.make_aware(naive_dt)
# 大于 2003-01-07
print("【大于】register_time > 2003-01-07")
qs_gt = User.objects.filter(register_time__gt=aware_dt).values("name", "age")
print(qs_gt)
# 小于 2003-01-07
print("【小于】register_time < 2003-01-07")
qs_lt = User.objects.filter(register_time__lt=aware_dt).values("name", "age")
print(qs_lt)
12.3.6总结
| 类别 | 查询方式 | 双下划线写法 | 说明 / SQL 对应 |
|---|---|---|---|
| 大小比较 | 大于 | field__gt=value |
SQL: field > value |
| 大小比较 | 大于等于 | field__gte=value |
SQL: field >= value |
| 大小比较 | 小于 | field__lt=value |
SQL: field < value |
| 大小比较 | 小于等于 | field__lte=value |
SQL: field <= value |
| 多值匹配 / 或条件 | 属于集合 | field__in=(val1, val2, ...) |
SQL: field IN (...) |
| 范围查询 | BETWEEN | field__range=(start, end) |
SQL: field BETWEEN start AND end |
| 模糊查询 | 包含(区分大小写) | field__contains="xxx" |
SQL: LIKE '%xxx%'(区分大小写) |
| 模糊查询 | 包含(不区分大小写) | field__icontains="xxx" |
SQL: ILIKE '%xxx%' 或大小写不敏感 LIKE |
| 字符串开头匹配 | 开头 | field__startswith="xxx" |
SQL: LIKE 'xxx%' |
| 字符串结尾匹配 | 结尾 | field__endswith="xxx" |
SQL: LIKE '%xxx' |
| 日期查询 | 按日 | field__day=DD |
SQL: DAY(field)=DD |
| 日期查询 | 按月 | field__month=MM |
SQL: MONTH(field)=MM |
| 日期查询 | 按年 | field__year=YYYY |
SQL: YEAR(field)=YYYY |
| 日期比较 | 大于 | field__gt=aware_datetime |
注意时区 aware datetime |
| 日期比较 | 小于 | field__lt=aware_datetime |
注意时区 aware datetime |

12.4数据库表关系
12.4.1简介
Django 中三种主要表关系:
| 关系类型 | Python / Django 实现 | 场景示例 | 说明 |
|---|---|---|---|
| 一对一(OneToOne) | OneToOneField 或 ForeignKey(..., unique=True) |
用户 ↔ 用户详情 | 每个用户只有一个详情,每条详情只对应一个用户 |
| 一对多(ForeignKey) | ForeignKey |
部门 ↔ 员工 | 多个员工属于同一个部门,但每个员工只属于一个部门 |
| 多对多(ManyToMany) | ManyToManyField 或手动建中间表 |
作者 ↔ 图书 | 一个作者可以写多本书,一本书也可以有多个作者 |
注意:
- 外键(ForeignKey)总是指向 “多” 端的表。
- 多对多关系需要 第三张表 来存储关联关系。
12.4.2手动创建第三张表实现多对多
多对多关系(Many-to-Many)
- 一个作者可以写多本书
- 一本书可以有多个作者
手动创建中间表(BookToAuthor1)
- Django 会自动创建中间表,但你可以显式定义
- 优势:可以在中间表增加额外字段,例如
create_time、role(作者角色)等 - 缺点:需要手动管理关联,操作稍微复杂一些
# ======================================================
# 【二】方案一:手动创建第三张表(显式管理多对多) +1
# ======================================================
from django.db import models
class Book1(models.Model):
# 书名,最大长度 255
title = models.CharField(max_length=255, verbose_name="书籍名")
# 价格(字符串类型,可存储带单位的价格,例如 "100元")
price = models.CharField(max_length=255, verbose_name="书籍价格")
# 创建时间,首次保存时自动添加
create_time = models.DateTimeField(auto_now_add=True)
# 更新时间,每次保存时自动更新
update_time = models.DateTimeField(auto_now=True)
class Meta:
db_table = "book1"
class Author1(models.Model):
# 作者姓名
name = models.CharField(max_length=32, verbose_name="姓名")
# 作者年龄
age = models.IntegerField(verbose_name="年龄")
# 创建时间,首次保存时自动添加
create_time = models.DateTimeField(auto_now_add=True)
# 更新时间,每次保存时自动更新
update_time = models.DateTimeField(auto_now=True)
class Meta:
db_table = "author1"
class BookToAuthor1(models.Model):
# 外键指向 Author1,作者被删除时关联记录也删除
author = models.ForeignKey(to="Author1", on_delete=models.CASCADE)
# 外键指向 Book1,书籍被删除时关联记录也删除
book = models.ForeignKey(to="Book1", on_delete=models.CASCADE)
# 创建时间
create_time = models.DateTimeField(auto_now_add=True)
# 更新时间
update_time = models.DateTimeField(auto_now=True)
class Meta:
db_table = "author_to_book1"

12.4.3ManyToMany和自定义through表实现多对多
多对多关系(Many-to-Many)
- 一个作者可以写多本书
- 一本书可以有多个作者
Django 风格管理
- 使用
ManyToManyField建立关系 - 通过
through指定自定义中间表 through_fields指定顺序((author, book)),保证 ORM 知道字段对应关系
优势
- 保留 Django ORM 的多对多便捷操作
- 可以在中间表添加额外字段
- 正向和反向查询更直观
# ======================================================
# 【三】方案二:ManyToMany + 自定义 through 表 +2
# ======================================================
class Book2(models.Model):
# 书名
title = models.CharField(max_length=255, verbose_name="书籍名")
# 价格
price = models.CharField(max_length=255, verbose_name="书籍价格")
create_time = models.DateTimeField(auto_now_add=True)
update_time = models.DateTimeField(auto_now=True)
class Meta:
db_table = "book2"
class Author2(models.Model):
# 作者姓名
name = models.CharField(max_length=32, verbose_name="姓名")
# 作者年龄
age = models.IntegerField(verbose_name="年龄")
create_time = models.DateTimeField(auto_now_add=True)
update_time = models.DateTimeField(auto_now=True)
# ManyToManyField 指向 Book2,通过自定义中间表 BookToAuthor2
# through_fields 顺序必须对应中间表字段顺序 (author, book)
book = models.ManyToManyField(
to="Book2",
through="BookToAuthor2",
through_fields=("author", "book")
)
class Meta:
db_table = "author2"
class BookToAuthor2(models.Model):
# 外键指向 Author2
author = models.ForeignKey(to="Author2", on_delete=models.CASCADE)
# 外键指向 Book2
book = models.ForeignKey(to="Book2", on_delete=models.CASCADE)
create_time = models.DateTimeField(auto_now_add=True)
update_time = models.DateTimeField(auto_now=True)
class Meta:
db_table = "author_to_book2"

12.4.4Django自动创建第三张表实现多对多
多对多关系(Many-to-Many)
- 一个作者可以写多本书
- 一本书可以有多个作者
Django 自动管理中间表
- 使用
ManyToManyField即可 - Django 会 自动创建第三张表 来管理关系
- 无需显式定义中间表
特点
- 简单、快捷
- 适合普通项目,不需要在中间表添加额外字段
- ORM 正向/反向查询自动支持
# ======================================================
# 【四】方案三:Django 自动创建第三张表 +3
# ======================================================
class Book3(models.Model):
# 书名
title = models.CharField(max_length=255, verbose_name="书籍名")
# 价格
price = models.CharField(max_length=255, verbose_name="书籍价格")
create_time = models.DateTimeField(auto_now_add=True)
update_time = models.DateTimeField(auto_now=True)
class Meta:
db_table = "book3"
class Author3(models.Model):
# 作者姓名
name = models.CharField(max_length=32, verbose_name="姓名")
# 作者年龄
age = models.IntegerField(verbose_name="年龄")
create_time = models.DateTimeField(auto_now_add=True)
update_time = models.DateTimeField(auto_now=True)
# ManyToManyField 默认会自动生成第三张表
book = models.ManyToManyField(to="Book3")
class Meta:
db_table = "author3"

12.4.5三种方式总结
正向 vs 反向查询区分
| 查询类型 | 含义 | 示例 |
|---|---|---|
| 正向查询 | 从模型定义字段出发 | author.book.all() |
| 反向查询 | 从另一端访问关联 | book.author3_set.all()(或使用 related_name 自定义) |
总结对比表
| 方案 | 中间表 | 可扩展字段 | ORM 使用 | 适用场景 |
|---|---|---|---|---|
| 方案一 | 显式手动 | 可以自定义 | 复杂,需要手动管理 | 企业级复杂项目,业务逻辑多 |
| 方案二 | 自定义 through | 可以自定义 | ORM 支持正向/反向查询 | 企业级项目 + 中间表字段扩展 |
| 方案三 | 自动生成 | 不可扩展 | ORM 自动管理,简单 | 简单项目,快速开发 |
12.4.6数据库迁移时删除migrations文件夹还是no changed
去你的每一个app下的 admin.py

复制admin模块的绝对路径

定位到 admin 模块的 migrations 文件夹

删除 migrations 文件夹下 除了 init.py 以外的所有文件
再重新输入迁移命令

总结
# 删除数据库中不想要的第三张表/某一张表
# 再次迁移发现 迁移不成功 每次都提示 no changed
# [1] 直接将数据库删除
# [2] 看每一个 app 下面的 migrations 文件夹
# [3] 删除每一个 app 下 的 migrations 文件夹 下的除了 __init__.py 以外的所有文件
# ----------- 上面不生效执行下边的
# [4] 如果仍然提示 no changed
# [5] 去你的每一个app下的 admin.py
# from django.contrib import admin
# [6] 按住 ctrl 进入到 admin 模块中
# [7] 定位到 admin 模块的 migrations 文件夹
# [8] 删除 migrations 文件夹下 除了 __init__.py 以外的所有文件
# [9] 再重新迁移
12.4.7多对多关系操作
12.4.7.1纯手动第三张表(Django不识别为多对多)
特点:
- Django 不知道是多对多
- 没有 add/remove/set
- 完全手动操作
- 只能手动添加:
BookToAuthor1.objects.create(author=a1, book=b1)
models.py
# 定义图书模型 Book1
class Book1(models.Model):
# 书名,最大长度为32个字符
name = models.CharField(max_length=32, verbose_name="书籍名称")
# 书价,最大长度为32个字符,建议改为 DecimalField 用于存储价格
price = models.CharField(max_length=32, verbose_name="书籍价格")
# 定义作者模型 Author1
class Author1(models.Model):
# 作者名字,最大长度为32个字符
name = models.CharField(max_length=32, verbose_name="作者名字")
# 作者年龄,最大长度为32个字符,建议改为 IntegerField 或者 CharField 用于存储年龄
age = models.CharField(max_length=32, verbose_name="作者年龄")
# 定义图书与作者的多对多关系模型 BookToAuthor1
class BookToAuthor1(models.Model):
# 外键引用 Author1 表,表示该图书对应的作者
# on_delete=models.CASCADE 实现的是 级联删除,即当父表(外键指向的表)的数据删除时,所有引用该数据的子表数据也会被删除。
author = models.ForeignKey("Author1", on_delete=models.CASCADE)
# 外键引用 Book1 表,表示该作者创作的书籍
book = models.ForeignKey("Book1", on_delete=models.CASCADE)
示例
# ======================================================
# 【一】Django 环境初始化(脚本运行必备)
# ======================================================
import random
def reset_auto_increment(models_list):
"""
models_list: 传入需要重置的模型列表
"""
with connection.cursor() as cursor:
engine = connection.settings_dict['ENGINE']
for model in models_list:
table = model._meta.db_table # 获取真实表名
# ==========================
# SQLite
# ==========================
if 'sqlite' in engine:
cursor.execute(
f"DELETE FROM sqlite_sequence WHERE name='{table}';"
)
# ==========================
# MySQL
# ==========================
elif 'mysql' in engine:
cursor.execute(
f"ALTER TABLE {table} AUTO_INCREMENT = 1;"
)
# ==========================
# PostgreSQL
# ==========================
elif 'postgresql' in engine:
cursor.execute(
f"ALTER SEQUENCE {table}_id_seq RESTART WITH 1;"
)
print("✅ 自增 ID 已重置")
if __name__ == '__main__':
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo08.settings')
django.setup()
from user.models import Book1, Author1, BookToAuthor1
from django.db import connection
# ======================================================
# 【零】清空表 & 重置自增 ID
# ======================================================
# 1删除表数据(顺序要注意:先从表底层删除数据)
BookToAuthor1.objects.all().delete()
Book1.objects.all().delete()
Author1.objects.all().delete()
# 2.删除表数据(顺序要注意:先从表底层删除数据)
reset_auto_increment([Book1, Author1, BookToAuthor1])
print("✅ 所有表已清空,自增 ID 已重置")
for i in range(5):
Book1.objects.create(name=f"Python实战{i}", price=round(random.uniform(100, 200), 2))
Author1.objects.create(name=f"peng{i}", age=i + 10)
print("====== 方式一: 手动创建第三张表 ======")
# 增加
b1 = Book1.objects.get(id=1)
a1 = Author1.objects.get(id=1)
BookToAuthor1.objects.create(book=b1, author=a1)
b2 = Book1.objects.get(name="Python实战1")
a2 = Author1.objects.get(name="peng1")
BookToAuthor1.objects.create(book=b2, author=a2)
# 查询
res = list(BookToAuthor1.objects.all())
print("手动格式化输出")
for item in res:
print(
f"id={item.id}, "
f"book_id={item.book_id}, "
f"author_id={item.author_id}"
)
print("输出关联对象")
for item in res:
print(
f"id={item.id}, "
f"book={item.book.name}, "
f"author={item.author.name}"
)
print("values")
res = BookToAuthor1.objects.values()
print(list(res))
# 删
BookToAuthor1.objects.filter(book=b2).delete()
res = BookToAuthor1.objects.values()
print("删除 b2 后")
print(list(res))
# 改(删+加)
BookToAuthor1.objects.filter(book=b1).delete()
BookToAuthor1.objects.create(author=a1, book=b2)
print("删 + 加")
res = BookToAuthor1.objects.values()
print(list(res))

12.4.7.2半自动through
特点:
-
Django 识别为多对多
-
可扩展中间表字段
-
不能用 add/remove(必须手动建中间表)
-
只能手动添加:
BookToAuthor2.objects.create(author=a1, book=b1) -
支持查询:
a1.book.all()
models.py
# 定义图书模型 Book2
class Book2(models.Model):
# 书名,最大长度为32个字符
name = models.CharField(max_length=32, verbose_name="书籍名称")
# 书价,最大长度为32个字符,建议改为 DecimalField 用于存储价格
price = models.CharField(max_length=32, verbose_name="书籍价格")
# 定义作者模型 Author2
class Author2(models.Model):
# 作者名字,最大长度为32个字符
name = models.CharField(max_length=32, verbose_name="作者名字")
# 作者年龄,最大长度为32个字符,建议改为 IntegerField 或者 CharField 用于存储年龄
age = models.CharField(max_length=32, verbose_name="作者年龄")
# 通过 `ManyToManyField` 关系,表示一个作者可以有多本书
# `through` 参数指定了通过一个中间表 `BookToAuthor2` 来管理这个多对多关系
# `through_fields` 参数指定中间表中的字段,这里是 ('author', 'book')
book = models.ManyToManyField(
to="Book2", # 关联的目标表是 `Book2`
through="BookToAuthor2", # 使用 `BookToAuthor2` 作为多对多的中间表
through_fields=("author", "book") # 指定中间表中的外键字段:`author` 和 `book`
)
# 定义图书与作者的多对多关系模型 BookToAuthor2
class BookToAuthor2(models.Model):
# 外键,引用 `Author2` 表,表示该记录对应的作者
author = models.ForeignKey("Author2", on_delete=models.CASCADE)
# 外键,引用 `Book2` 表,表示该记录对应的书籍
book = models.ForeignKey("Book2", on_delete=models.CASCADE)
示例
# ======================================================
# 【一】Django 环境初始化(脚本运行必备)
# ======================================================
if __name__ == '__main__':
import os
import django
import random
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo08.settings')
django.setup()
from user.models import Book2, Author2, BookToAuthor2
from utils import reset_auto_increment
# ======================================================
# 【零】清空表 & 重置自增 ID
# ======================================================
BookToAuthor2.objects.all().delete()
Book2.objects.all().delete()
Author2.objects.all().delete()
reset_auto_increment([Book2, Author2, BookToAuthor2])
print("✅ 所有表已清空,自增 ID 已重置\n")
# ======================================================
# 【一】创建测试数据
# ======================================================
print("====== 创建书籍和作者 ======")
for i in range(5):
book = Book2.objects.create(name=f"Python实战{i}", price=round(random.uniform(100, 200), 2))
author = Author2.objects.create(name=f"peng{i}", age=i + 10)
print(f"创建 Book2 -> id:{book.id}, name:{book.name}, price:{book.price}")
print(f"创建 Author2 -> id:{author.id}, name:{author.name}, age:{author.age}")
print()
# ======================================================
# 【二】ManyToMany关系操作(自定义 through 表)
# ======================================================
print("====== ManyToMany through 表操作 ======")
a1 = Author2.objects.get(id=1)
a2 = Author2.objects.get(id=2)
b1 = Book2.objects.get(id=1)
b2 = Book2.objects.get(id=2)
rel1 = BookToAuthor2.objects.create(author=a1, book=b1)
rel2 = BookToAuthor2.objects.create(author=a2, book=b2)
print(f"创建关系 -> Author:{a1.name} 与 Book:{b1.name}")
print(f"创建关系 -> Author:{a2.name} 与 Book:{b2.name}")
print()
# ======================================================
# 【查】
# ======================================================
print("====== 查询作者的书籍 ======")
books_of_a1 = list(a1.book.all())
for book in books_of_a1:
print(f"作者 {a1.name} 的书 -> id:{book.id}, name:{book.name}, price:{book.price}")
print()
# ======================================================
# 【删】
# ======================================================
print("====== 删除 Book2 id=2 的关系 ======")
BookToAuthor2.objects.filter(book=b2).delete()
print(f"Book2 id=2 的关系已删除")
print(f"当前所有关系: {[{'author': r.author.name, 'book': r.book.name} for r in BookToAuthor2.objects.all()]}")
print()
# ======================================================
# 【改】
# ======================================================
print("====== 修改 Book2 id=1 的关系角色 ======")
# 其实都是先删后改
# 删除原来的关系
BookToAuthor2.objects.filter(book=b1, author=a1).delete()
# 创建新的关系
BookToAuthor2.objects.create(book=b1, author=a2)
# 打印
rel = BookToAuthor2.objects.get(book=b1)
print(f"修改后的关系 -> Author:{rel.author.name}, Book:{rel.book.name}")

12.4.7.3纯自动
特点:
- Django 自动创建中间表
- 支持完整 ORM 操作
- 无法扩展中间表字段
- 支持add、remove、set、all
a1.book.add(b1)
a1.book.remove(b1)
a1.book.set([b1, b2])
a1.book.all()
models.py
# 定义图书模型 Book3
class Book3(models.Model):
# 书名,最大长度为32个字符
name = models.CharField(max_length=32, verbose_name="书籍名称")
# 书价,最大长度为32个字符,建议改为 DecimalField 用于存储价格
price = models.CharField(max_length=32, verbose_name="书籍价格")
# 定义作者模型 Author3
class Author3(models.Model):
# 作者名字,最大长度为32个字符
name = models.CharField(max_length=32, verbose_name="作者名字")
# 作者年龄,最大长度为32个字符,建议改为 IntegerField 或者 CharField 用于存储年龄
age = models.CharField(max_length=32, verbose_name="作者年龄")
# 通过 `ManyToManyField` 关系,表示一个作者可以有多本书
# 默认情况下,Django 会自动管理多对多关系,创建一个中间表来处理这个关系
book = models.ManyToManyField(to="Book3") # 关联的目标表是 `Book3`
示例
# ======================================================
# 【一】Django 环境初始化(脚本运行必备)
# ======================================================
if __name__ == '__main__':
import os
import django
import random
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo08.settings')
django.setup()
from user.models import Author3, Book3
from utils import reset_auto_increment
# ======================================================
# 【零】清空表 & 重置自增 ID
# ======================================================
Book3.objects.all().delete()
Author3.objects.all().delete()
reset_auto_increment([Book3, Author3])
print("✅ 所有表已清空,自增 ID 已重置\n")
# ======================================================
# 【一】创建测试数据
# ======================================================
print("====== 创建书籍和作者 ======")
for i in range(5):
book = Book3.objects.create(name=f"Python实战{i + 1}", price=round(random.uniform(100, 200), 2))
author = Author3.objects.create(name=f"peng{i + 1}", age=i + 10)
print(f"创建 Book2 -> id:{book.id}, name:{book.name}, price:{book.price}")
print(f"创建 Author2 -> id:{author.id}, name:{author.name}, age:{author.age}")
print()
# ======================================================
# 【二】ManyToMany关系操作
# ======================================================
print("====== ManyToMany 操作 ======")
a1 = Author3.objects.get(id=1)
a2 = Author3.objects.get(id=2)
b1 = Book3.objects.get(id=1)
b2 = Book3.objects.get(id=2)
# —— 增
print(">> 增加关系: a1 添加 b1, b2")
a1.book.add(b1, b2)
print(f"a1 的书籍: {[f'id:{b.id}, name:{b.name}, price:{b.price}' for b in a1.book.all()]}\n")
# —— 查
print(">> 查询 a1 的书籍")
books_of_a1 = a1.book.all()
for b in books_of_a1:
print(f"id:{b.id}, name:{b.name}, price:{b.price}")
print()
# —— 删
print(">> 删除关系: a1 移除 b2")
a1.book.remove(b2)
print(f"a1 的书籍: {[f'id:{b.id}, name:{b.name}, price:{b.price}' for b in a1.book.all()]}\n")
# —— 改(set)
print(">> 修改关系: a1 的书籍 set 为 [b2, b3]]")
b3 = Book3.objects.get(id=3)
a1.book.set([b2, b3])
print(f"a1 的书籍: {[f'id:{b.id}, name:{b.name}, price:{b.price}' for b in a1.book.all()]}\n")
# —— 清空
print(">> 清空关系: a1 清空书籍")
a1.book.clear()
print(f"a1 的书籍: {[f'id:{b.id}, name:{b.name}, price:{b.price}' for b in a1.book.all()]}\n")

12.4.8一对多关系操作
总结
- 外键字段本质:数据库中是 publish_id
- Django 提供两种操作方式:
- publish_id(底层)
- publish(对象,推荐)
- 级联删除:
- on_delete=models.CASCADE
- 删除主表,从表数据一起删除
- 查询方向
- 正向:book.publish
- 反向:publish.book_set
models.py
# =========================
# 📚 图书表(核心表)
# =========================
class Book(models.Model):
# 书名字段
name = models.CharField(
max_length=32,
verbose_name="书籍名称"
)
# 价格(⚠️注意:用CharField不太合理,建议用DecimalField)
price = models.CharField(
max_length=32,
verbose_name="书籍价格"
)
# =========================
# ⭐ 多对多关系(Book ↔ Author)
# =========================
# 含义:
# 一本书可以有多个作者
# 一个作者可以写多本书
#
# Django底层做了什么?
# 👉 自动帮你创建“第三张表”(中间表)
# 表结构类似:
# book_author
# id
# book_id (外键 -> Book)
# author_id (外键 -> Author)
#
# 使用方式:
# book.author.add(作者对象)
# book.author.remove(作者对象)
# book.author.set([作者1, 作者2])
#
# ⚠️注意:
# 1. 不会在 Book 表中生成字段(不像 ForeignKey)
# 2. 数据存在第三张表中
author = models.ManyToManyField(
to="Author", # 关联 Author 表
)
# =========================
# ⭐ 一对多关系(Book → Publish)
# =========================
# 含义:
# 一本书只能属于一个出版社
# 一个出版社可以出版多本书
#
# Django底层做了什么?
# 👉 在 Book 表中新增字段:
# publish_id
#
# 等价SQL:
# publish_id INT 外键
#
# on_delete=models.CASCADE:
# 👉 如果出版社删除,书也会被删除(级联删除)
#
# ⚠️问题:
# default="" 不合理(外键应该是ID)
# 正确写法建议:
# null=True 或 指定一个合法ID
publish = models.ForeignKey(
to="Publish",
on_delete=models.CASCADE,
default="" # ❌ 不推荐
)
class Meta:
db_table = "book" # 指定数据库表名
# =========================
# ✍️ 作者表
# =========================
class Author(models.Model):
name = models.CharField(
max_length=32,
verbose_name="作者名字"
)
age = models.CharField(
max_length=32,
verbose_name="作者年龄"
)
# =========================
# ⭐ 一对一关系(Author ↔ AuthorDetail)
# =========================
# 含义:
# 一个作者只对应一个详情
# 一个详情只属于一个作者
#
# Django底层做了什么?
# 👉 在 Author 表中新增字段:
# detail_id(唯一 unique=True)
#
# 等价SQL:
# detail_id INT UNIQUE 外键
#
# null=True:
# 👉 可以为空(作者可以暂时没有详情)
#
# on_delete=models.CASCADE:
# 👉 删除详情 → 作者也会被删除(级联)
detail = models.OneToOneField(
to="AuthorDetail",
on_delete=models.CASCADE,
null=True
)
class Meta:
db_table = "author"
# =========================
# 🏢 出版社表
# =========================
class Publish(models.Model):
name = models.CharField(
max_length=32,
verbose_name="出版社名字"
)
addr = models.CharField(
max_length=32,
verbose_name="出版社地址"
)
phone = models.CharField(
max_length=11,
verbose_name="出版社电话"
)
class Meta:
db_table = "publish"
# =========================
# 👤 作者详情表
# =========================
class AuthorDetail(models.Model):
addr = models.CharField(
max_length=32,
verbose_name="作者地址"
)
phone = models.CharField(
max_length=13,
verbose_name="作者电话"
)
gender = models.CharField(
max_length=11,
verbose_name="作者性别"
)
class Meta:
db_table = "author_detail"
示例
# ======================================================
# 【一】Django 环境初始化(脚本运行必备)
# ======================================================
import os
import django
from django.forms.models import model_to_dict
if __name__ == '__main__':
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo08.settings')
django.setup()
from user.models import Author, AuthorDetail, Book, Publish
from django.db import connection
# ======================================================
# 【零】清空表 & 重置自增 ID
# ======================================================
# 1️⃣ 删除表数据(顺序要注意:先从表底层删除数据)
Book.objects.all().delete()
Publish.objects.all().delete()
Author.objects.all().delete()
AuthorDetail.objects.all().delete()
# 2️⃣ 重置自增 ID(不同数据库方式不同)
with connection.cursor() as cursor:
# SQLite
if 'sqlite' in connection.settings_dict['ENGINE']:
cursor.execute("DELETE FROM sqlite_sequence WHERE name='book';")
cursor.execute("DELETE FROM sqlite_sequence WHERE name='publish';")
cursor.execute("DELETE FROM sqlite_sequence WHERE name='author';")
cursor.execute("DELETE FROM sqlite_sequence WHERE name='author_detail';")
# MySQL
elif 'mysql' in connection.settings_dict['ENGINE']:
cursor.execute("ALTER TABLE book AUTO_INCREMENT = 1;")
cursor.execute("ALTER TABLE publish AUTO_INCREMENT = 1;")
cursor.execute("ALTER TABLE author AUTO_INCREMENT = 1;")
cursor.execute("ALTER TABLE author_detail AUTO_INCREMENT = 1;")
# PostgreSQL
elif 'postgresql' in connection.settings_dict['ENGINE']:
cursor.execute("ALTER SEQUENCE book_id_seq RESTART WITH 1;")
cursor.execute("ALTER SEQUENCE publish_id_seq RESTART WITH 1;")
cursor.execute("ALTER SEQUENCE author_id_seq RESTART WITH 1;")
cursor.execute("ALTER SEQUENCE author_detail_id_seq RESTART WITH 1;")
print("✅ 所有表已清空,自增 ID 已重置")
# ======================================================
# 【三】添加外键关系(Create)
# ======================================================
# ------------------------------------------
# 方式一:使用 publish_id(推荐理解底层)
# ------------------------------------------
publish_obj1 = Publish.objects.create(name="北方出版社", addr="北京市", phone="18818889999")
Book.objects.create(name="西游记", price="666", publish_id=publish_obj1.id)
publish_obj2 = Publish.objects.create(name="南方出版社", addr="上海市", phone="18818888888")
Book.objects.create(name="三国演义", price="777", publish_id=publish_obj2.id)
publish_obj3 = Publish.objects.create(name="天津出版社", addr="天津市", phone="18818887777")
Book.objects.create(name="儒林外史", price="888", publish_id=publish_obj3.id)
# ------------------------------------------
# 方式二:直接使用对象(推荐)
# ------------------------------------------
publish_obj4 = Publish.objects.get(name="北方出版社")
Book.objects.create(name="红楼梦", price="999", publish=publish_obj4)
publish_obj5 = Publish.objects.get(name="南方出版社")
Book.objects.create(name="水浒传", price="1000", publish=publish_obj5)
publish_obj6 = Publish.objects.get(name="天津出版社")
Book.objects.create(name="聊斋志异", price="1111", publish=publish_obj6)
print("\n📚 添加外键关系后 Book 表:")
for book in Book.objects.all():
print(f"id={book.id}, name={book.name}, price={book.price}, publish_id={book.publish_id}")
print("\n🏢 添加外键关系后 Publish 表:")
for pub in Publish.objects.all():
print(f"id={pub.id}, name={pub.name}, addr={pub.addr}, phone={pub.phone}")
# ======================================================
# 【四】删除外键关系(Delete)
# ======================================================
# ------------------------------------------
# 方式一:删除 Book(不会影响 Publish)
# ------------------------------------------
Book.objects.filter(name="西游记").delete()
# ------------------------------------------
# 方式二:删除 Publish(级联删除 Book)
# 原因:on_delete=models.CASCADE
# ------------------------------------------
Publish.objects.filter(id=2).delete()
print("\n📚 删除操作后 Book 表:")
for book in Book.objects.all():
print(f"id={book.id}, name={book.name}, price={book.price}, publish_id={book.publish_id}")
print("\n🏢 删除操作后 Publish 表:")
for pub in Publish.objects.all():
print(f"id={pub.id}, name={pub.name}, addr={pub.addr}, phone={pub.phone}")
# ======================================================
# 【五】修改外键关系(Update)
# ======================================================
# ------------------------------------------
# 准备新的出版社对象
# ------------------------------------------
publish_obj5 = Publish.objects.create(name="北京出版社", addr="北京市", phone="18818887777")
Book.objects.filter(name="儒林外史").update(publish_id=publish_obj5.id)
publish_obj6 = Publish.objects.create(name="上海出版社", addr="上海市", phone="18818886666")
Book.objects.filter(name="红楼梦").update(publish=publish_obj6)
print("\n📚 修改外键关系后 Book 表:")
for book in Book.objects.all():
print(f"id={book.id}, name={book.name}, price={book.price}, publish_id={book.publish_id}")
# ======================================================
# 【六】查询外键关系(Query)
# ======================================================
# ------------------------------------------
# 正向查询(Book → Publish)
# ------------------------------------------
book = Book.objects.get(name="红楼梦")
print("\n📖 查询红楼梦信息:")
print("Book 对象详细信息:", model_to_dict(book))
print("出版社对象详细信息:", model_to_dict(book.publish))
# ------------------------------------------
# 反向查询(Publish → Book)
# ------------------------------------------
publish = Publish.objects.get(name="上海出版社")
print("\n📖 查询上海出版社下所有书:")
for b in publish.book_set.all():
print(f"id={b.id}, name={b.name}, price={b.price}")

12.5多表查询
Django 多表查询本质就一句话:正向查字段,反向查表名小写_set(默认)
关系 & 查询方式总览
| 关系类型 | 正向查询 | 反向查询 |
|---|---|---|
| 一对多(ForeignKey) | obj.字段 |
obj.子表_set |
| 多对多(ManyToMany) | obj.字段.all() |
obj.表_set.all() |
| 一对一(OneToOne) | obj.字段 |
obj.表名 |
基本原则
| 查询方向 | 含义 | 用法 |
|---|---|---|
| 正向查询 | 从 拥有字段的模型 去查 | 直接用字段名 |
| 反向查询 | 从 被引用的模型 去查 | 用默认 _set 或 related_name |
示例
import os
import django
import random
from scripts.utils import reset_auto_increment
# 初始化 Django 环境
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo08.settings')
django.setup()
from user.models import Book, Author, Publish, AuthorDetail
# 随机生成手机号
def get_random_phone():
first = "1" + str(random.randint(3, 9))
rest = "".join(str(random.randint(0, 9)) for _ in range(9))
return first + rest
# 定义城市列表
def get_random_city():
"""返回一个随机城市名字"""
cities = [
"北京", "上海", "天津", "重庆", "广州", "深圳"
]
return random.choice(cities)
# ======================================================
# 📝 清理历史数据(可选)
# ======================================================
Book.objects.all().delete()
Author.objects.all().delete()
Publish.objects.all().delete()
AuthorDetail.objects.all().delete()
reset_auto_increment([Book, Author, Publish, AuthorDetail])
print("所有表已清空,自增 ID 已重置\n")
# ======================================================
# 📌 插入测试数据
# ======================================================
# 1️⃣ 创建出版社
pub1 = Publish.objects.create(name="北方出版社", addr="北京市", phone="18818889999")
pub2 = Publish.objects.create(name="南方出版社", addr="上海市", phone="18818888888")
pub3 = Publish.objects.create(name="天津出版社", addr="天津市", phone="18818887777")
# 2️⃣ 创建书籍
book1 = Book.objects.create(name="西游记", price="666", publish=pub1)
book2 = Book.objects.create(name="三国演义", price="777", publish=pub2)
book3 = Book.objects.create(name="儒林外史", price="888", publish=pub3)
book4 = Book.objects.create(name="红楼梦", price="999", publish=pub1)
book5 = Book.objects.create(name="水浒传", price="1000", publish=pub2)
book6 = Book.objects.create(name="聊斋志异", price="1111", publish=pub3)
# 3️⃣ 创建作者详情
detail_list = []
for i in range(1, 6):
d = AuthorDetail.objects.create(
addr=get_random_city(),
phone=get_random_phone(),
gender="男" if i % 2 == 1 else "女"
)
detail_list.append(d)
# 4️⃣ 创建作者并关联 AuthorDetail
author_list = []
for i, detail in enumerate(detail_list, start=1):
a = Author.objects.create(
name=f"peng{i}",
age=str(20 + i),
detail=detail
)
author_list.append(a)
# 5️⃣ 多对多关系:给书籍分配作者
book1.author.add(author_list[0], author_list[1]) # 西游记: 作者1, 作者2
book2.author.add(author_list[1], author_list[2]) # 三国演义: 作者2, 作者3
book4.author.add(author_list[0], author_list[2], author_list[3]) # 红楼梦: 作者1, 作者3, 作者4
book5.author.add(author_list[3], author_list[4]) # 水浒传: 作者4, 作者5
book3.author.add(author_list[4]) # 儒林外史: 作者5
book6.author.add(author_list[0]) # 聊斋志异: 作者1
# ======================================================
# 📝 输出基础数据
# ======================================================
print("\n📚 Book 表:")
for book in Book.objects.all():
print(f"id={book.id}, name={book.name}, price={book.price}, publish={book.publish.name}")
print("\n🏢 Publish 表:")
for pub in Publish.objects.all():
print(f"id={pub.id}, name={pub.name}, addr={pub.addr}, phone={pub.phone}")
print("\n👤 Author 表:")
for author in Author.objects.all():
print(f"id={author.id}, name={author.name}, age={author.age}, detail_id={author.detail.id}")
print("\n👤 AuthorDetail 表:")
for detail in AuthorDetail.objects.all():
print(f"id={detail.id}, addr={detail.addr}, phone={detail.phone}, gender={detail.gender}")
# ======================================================
# 🔹 正向查询 vs 反向查询(注释已修正)
# ======================================================
# 【1】正向查询:Book -> Publish (一对多)
book_obj = Book.objects.get(name="红楼梦")
print(f"\n--- 正向查询:Book -> Publish ---\n{book_obj.name} 出版社: {book_obj.publish.name}")
# 【2】反向查询多对多:Author -> Book
author_obj = Author.objects.get(name="peng1")
# book_set 是 Author 通过 Book.author 的反向查询
print(
f"\n--- 反向查询:Author -> Book (多对多) ---\n{author_obj.name} 的所有书籍: {[b.name for b in author_obj.book_set.all()]}")
# 【3】正向查询多对多:Book -> Author
book_obj = Book.objects.get(name="西游记")
# author 是 Book 模型定义的 ManyToManyField
print(f"\n--- 正向查询:Book -> Author (多对多) ---\n{book_obj.name} 的作者: {[a.name for a in book_obj.author.all()]}")
# 【4】反向查询一对多:Publish -> Book
publish_obj = Publish.objects.get(name="北方出版社")
# book_set 是 Publish 的反向查询
print(
f"\n--- 反向查询一对多:Publish -> Book ---\n{publish_obj.name} 出版的书籍: {[b.name for b in publish_obj.book_set.all()]}")
# 【5】反向查询多对多:Author -> Book
author_obj = Author.objects.get(name="peng3")
print(
f"\n--- 反向查询多对多:Author -> Book ---\n{author_obj.name} 出版的所有书籍: {[b.name for b in author_obj.book_set.all()]}")
# 【6】正向查询一对一:AuthorDetail -> Author
author_detail_obj = AuthorDetail.objects.all()[0]
# 通过 Author.one_to_one_field_name 默认名称 author
print(
f"\n--- 正向查询一对一:AuthorDetail -> Author ---\n电话 {author_detail_obj.phone} 对应作者: {author_detail_obj.author.name}")
# 【7】反向查询一对多:Publish -> Book
publish_obj = Publish.objects.get(name="南方出版社")
print(
f"\n--- 反向查询一对多:Publish -> Book ---\n{publish_obj.name} 出版的书籍: {[b.name for b in publish_obj.book_set.all()]}")

12.6基于下划线的夸表查询
注意点
- 正向查询永远使用模型字段名,不用模型类名。
- 反向查询要用 Django 默认生成的反向属性:
ForeignKey/ManyToManyField默认_setOneToOneField默认小写模型名
- 多级查询:可以连续使用
__- 如
author__detail__phone或author__book__id
- 如
values()返回字典列表,适合只取部分字段,节省性能。- 注意 N+1 问题:大数据量时,多级正向查询可以用
select_related或prefetch_related优化。
总结
_是 Django ORM 跨表查询的关键,左边当前模型字段,右边目标字段- 支持正向、反向、多级查询
- 适用于 ForeignKey、OneToOneField、ManyToManyField
- 使用
values()或values_list()获取字典或元组,减少内存占用 - 多对多关系和外键反向访问的默认命名规则很重要
示例
import os
import django
import random
from scripts.utils import reset_auto_increment
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo08.settings')
django.setup()
from user.models import Book, Author, Publish, AuthorDetail
# 随机生成手机号
def get_random_phone():
first = "1" + str(random.randint(3, 9))
rest = "".join(str(random.randint(0, 9)) for _ in range(9))
return first + rest
# 定义城市列表
def get_random_city():
"""返回一个随机城市名字"""
cities = [
"北京", "上海", "天津", "重庆", "广州", "深圳"
]
return random.choice(cities)
# ======================================================
# 0️⃣ 清空表 & 重置自增 ID
# ======================================================
Book.objects.all().delete()
Author.objects.all().delete()
reset_auto_increment([Book, Author, Publish])
print("所有表已清空,自增 ID 已重置\n")
# ======================================================
# 📌 插入测试数据
# ======================================================
# 1️⃣ 创建出版社
pub1 = Publish.objects.create(name="北方出版社", addr="北京市", phone="18818889999")
pub2 = Publish.objects.create(name="南方出版社", addr="上海市", phone="18818888888")
pub3 = Publish.objects.create(name="天津出版社", addr="天津市", phone="18818887777")
# 2️⃣ 创建书籍
book1 = Book.objects.create(name="西游记", price="666", publish=pub1)
book2 = Book.objects.create(name="三国演义", price="777", publish=pub2)
book3 = Book.objects.create(name="儒林外史", price="888", publish=pub3)
book4 = Book.objects.create(name="红楼梦", price="999", publish=pub1)
book5 = Book.objects.create(name="水浒传", price="1000", publish=pub2)
book6 = Book.objects.create(name="聊斋志异", price="1111", publish=pub3)
# 3️⃣ 创建作者详情
detail_list = []
for i in range(1, 6):
d = AuthorDetail.objects.create(
addr=get_random_city(),
phone=get_random_phone(),
gender="男" if i % 2 == 1 else "女"
)
detail_list.append(d)
# 4️⃣ 创建作者并关联 AuthorDetail
author_list = []
for i, detail in enumerate(detail_list, start=1):
a = Author.objects.create(
name=f"peng{i}",
age=str(20 + i),
detail=detail
)
author_list.append(a)
# 5️⃣ 多对多关系:给书籍分配作者
book1.author.add(author_list[0], author_list[1]) # 西游记: 作者1, 作者2
book2.author.add(author_list[1], author_list[2]) # 三国演义: 作者2, 作者3
book4.author.add(author_list[0], author_list[2], author_list[3]) # 红楼梦: 作者1, 作者3, 作者4
book5.author.add(author_list[3], author_list[4]) # 水浒传: 作者4, 作者5
book3.author.add(author_list[4]) # 儒林外史: 作者5
book6.author.add(author_list[0]) # 聊斋志异: 作者1
# 查询 peng 的手机号和作者姓名
print("【1】查询 peng1 的手机号和作者姓名")
# ======================================================
# 正向查询 (Current model → Related model)
# ======================================================
# 语法:
# detail__phone 当前模型字段名__目标模型字段名
# 规则:
# - 左边是“当前模型里的字段名”,这个字段是 ForeignKey / OneToOneField / ManyToManyField
# - 右边是“目标模型的字段名”
author_info = Author.objects.filter(name="peng1").values("name", "detail__phone")
print("正向:", list(author_info))
# ------------------------------------------------------
# 2反向查询 (Related model → Current model)
# ======================================================
# 当 OneToOneField / ForeignKey / ManyToManyField 没有指定 related_name 时:
# - OneToOneField → 反向属性 = 源模型类名小写
# - ForeignKey → 反向属性 = 源模型类名小写 + "_set"
# - ManyToManyField → 反向属性 = 源模型类名小写 + "_set"
author_info = AuthorDetail.objects.filter(author__name="peng1").values("author__name", "phone")
print("反向:", list(author_info))
# 查询书籍 ID 为1的出版社名字和书的名字
print("【2】查询书籍ID为1的出版社名字和书的名字")
book_info = Book.objects.filter(id=1).values("name", "publish__name")
print("正向:", list(book_info))
book_info = Publish.objects.filter(book__id=1).values("book__name", "name")
print("反向:", list(book_info))
# 查询书籍ID为1的作者名字
print("【3】查询书籍ID为1的作者名字")
book_info = Book.objects.filter(id=1).values("name", "author__name")
print("正向:", list(book_info))
book_info = Author.objects.filter(book__id=1).values("book__name", "name")
print("反向:", list(book_info))
# 查询书籍ID为1的作者手机号
print("【4】查询书籍ID为1的作者手机号")
# 正向查询永远是字段名,不是模型名
# author__name → Book 模型里定义的 ManyToManyField 字段名
# author__detail__phone → Author 模型里定义的 OneToOneField 字段名
book_info = Book.objects.filter(id=1).values("name", "author__name", "author__detail__phone")
print("正向:", list(book_info))
# 反向查询:Django 自动生成 反向属性名
# author → AuthorDetail 的 反向 OneToOne 属性(默认是模型类小写)
# book → Author 模型的 ManyToManyField 字段名 默认是 book_set
# 只能写 author__book__id,author__book_set__id 会报错,因为从 AuthorDetail 走 author → books,反向属性 book_set 只对 从 Book 出发 有效。
book_info = AuthorDetail.objects.filter(author__book__id=1).values("author__book__name", "author__name", "phone")
print("反向:", list(book_info))

12.7聚合查询
核心函数
| 函数 | 作用 |
|---|---|
Avg |
平均值 |
Sum |
总和 |
Max |
最大值 |
Min |
最小值 |
Count |
数量 |
F |
字段运算 |
Cast |
类型转换 |
示例
# ======================================================
# 【一】Django 环境初始化(脚本运行必备)
# ======================================================
if __name__ == '__main__':
import os
import random
import django
from django.db.models import Sum, Max, Min, Count, Avg, F
from django.db.models.functions import Cast
from django.db.models import FloatField
from scripts.utils import reset_auto_increment
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo08.settings')
django.setup()
from user.models import Book, Author, Publish, AuthorDetail
# ----------------- 随机工具函数 -----------------
def get_random_phone():
first = "1" + str(random.randint(3, 9))
rest = "".join(str(random.randint(0, 9)) for _ in range(9))
return first + rest
def get_random_city():
return random.choice(["北京", "上海", "天津", "重庆", "广州", "深圳"])
# ----------------- 清空表 & 重置自增 -----------------
Book.objects.all().delete()
Author.objects.all().delete()
AuthorDetail.objects.all().delete()
Publish.objects.all().delete()
reset_auto_increment([Book, Author, AuthorDetail, Publish])
print("✅ 所有表已清空,自增 ID 已重置\n")
# ----------------- 插入测试数据 -----------------
pub1 = Publish.objects.create(name="北方出版社", addr="北京市", phone="18818889999")
pub2 = Publish.objects.create(name="南方出版社", addr="上海市", phone="18818888888")
pub3 = Publish.objects.create(name="天津出版社", addr="天津市", phone="18818887777")
books_data = [
("西游记", "666", pub1),
("三国演义", "777", pub2),
("儒林外史", "888", pub3),
("红楼梦", "999", pub1),
("水浒传", "1000", pub2),
("聊斋志异", "1111", pub3)
]
book_objs = [Book.objects.create(name=name, price=price, publish=pub) for name, price, pub in books_data]
detail_list = [AuthorDetail.objects.create(addr=get_random_city(), phone=get_random_phone(), gender="男" if i % 2 else "女") for i in range(6)]
author_list = [Author.objects.create(name=f"peng{i+1}", age=str(20+i), detail=detail_list[i]) for i in range(5)]
# 多对多关系
book_objs[0].author.add(author_list[0], author_list[1]) # 西游记
book_objs[1].author.add(author_list[1], author_list[2]) # 三国演义
book_objs[2].author.add(author_list[4]) # 儒林外史
book_objs[3].author.add(author_list[0], author_list[2], author_list[3]) # 红楼梦
book_objs[4].author.add(author_list[3], author_list[4]) # 水浒传
book_objs[5].author.add(author_list[0]) # 聊斋志异
# ----------------- 查询 & 聚合 -----------------
print("📚 所有书籍对象:")
for book in Book.objects.all():
print(f"ID: {book.id}, 名称: {book.name}, 价格: {book.price}, 出版社: {book.publish.name}")
# 聚合统计(Cast 转换 price 字段为数字)
# 我们数据库的字段是 CharField 这里不类型转换会影响聚合结果
# 第一种方案: 把 price 改成数值类型(推荐 DecimalField 或 FloatField)
# price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="书籍价格")
# 第二种方案: 如果暂时不改字段类型,可以在聚合前转换
stats = Book.objects.aggregate(
avg_price=Avg(Cast("price", FloatField())),
sum_price=Sum(Cast("price", FloatField())),
max_price=Max(Cast("price", FloatField())),
min_price=Min(Cast("price", FloatField())),
count_books=Count("id")
)
print("\n📊 书籍统计信息:")
print(f"总数量: {stats['count_books']}")
print(f"平均价格: {stats['avg_price']:.2f}")
print(f"总价: {stats['sum_price']}")
print(f"最高价: {stats['max_price']}")
print(f"最低价: {stats['min_price']}")

12.8分组查询
annotate
- 给 QuerySet 按某个字段分组
- 并在每一组上做统计(Count / Sum / Avg 等)
本质 SQL
SELECT 分组字段, 聚合函数(...)
FROM 表
GROUP BY 分组字段
和 aggregate 的区别
| 对比 | annotate | aggregate |
|---|---|---|
| 作用 | 分组统计 | 全局统计 |
| 返回 | QuerySet | dict |
| 是否 GROUP BY | 否 | 是 |
示例
# ======================================================
# 【一】Django 环境初始化(脚本运行必备)
# ======================================================
if __name__ == '__main__':
import os
import random
import django
from django.db.models import Sum, Max, Min, Count, Avg, F
from django.db.models.functions import Cast
from django.db.models import FloatField
from scripts.utils import reset_auto_increment
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo08.settings')
django.setup()
from user.models import Book, Author, Publish, AuthorDetail
# ----------------- 随机工具函数 -----------------
def get_random_phone():
first = "1" + str(random.randint(3, 9))
rest = "".join(str(random.randint(0, 9)) for _ in range(9))
return first + rest
def get_random_city():
return random.choice(["北京", "上海", "天津", "重庆", "广州", "深圳"])
# ----------------- 清空表 & 重置自增 -----------------
Book.objects.all().delete()
Author.objects.all().delete()
AuthorDetail.objects.all().delete()
Publish.objects.all().delete()
reset_auto_increment([Book, Author, AuthorDetail, Publish])
print("✅ 所有表已清空,自增 ID 已重置\n")
# ----------------- 插入测试数据 -----------------
pub1 = Publish.objects.create(name="北方出版社", addr="北京市", phone="18818889999")
pub2 = Publish.objects.create(name="南方出版社", addr="上海市", phone="18818888888")
pub3 = Publish.objects.create(name="天津出版社", addr="天津市", phone="18818887777")
books_data = [
("西游记", "666", pub1),
("三国演义", "777", pub2),
("儒林外史", "888", pub3),
("红楼梦", "999", pub1),
("水浒传", "1000", pub2),
("聊斋志异", "1111", pub3)
]
book_objs = [Book.objects.create(name=name, price=price, publish=pub) for name, price, pub in books_data]
detail_list = [AuthorDetail.objects.create(addr=get_random_city(), phone=get_random_phone(), gender="男" if i % 2 else "女") for i in range(6)]
author_list = [Author.objects.create(name=f"peng{i+1}", age=str(20+i), detail=detail_list[i]) for i in range(5)]
# 多对多关系
book_objs[0].author.add(author_list[0], author_list[1]) # 西游记
book_objs[1].author.add(author_list[1], author_list[2]) # 三国演义
book_objs[2].author.add(author_list[4]) # 儒林外史
book_objs[3].author.add(author_list[0], author_list[2], author_list[3]) # 红楼梦
book_objs[4].author.add(author_list[3], author_list[4]) # 水浒传
book_objs[5].author.add(author_list[0]) # 聊斋志异
# =========================
# 【1】统计每一本书的作者数量
# =========================
# 方法一:默认键名 author__count
author_count_default = Book.objects.annotate(Count("author")).values()
"""
select id,name,a.cnt from book b
left join (select book_id,COUNT(book_id) cnt from book_author GROUP BY book_id )
a on a.book_id = b.id
"""
# select * from book_author group by book_id
print("【方法一】每本书作者数量(默认键名):")
print(list(author_count_default))
# 方法二:自定义键名 author_num
"""
select id,name,a.cnt as author_num from book b
left join (select book_id,COUNT(book_id) cnt from book_author GROUP BY book_id )
a on a.book_id = b.id
"""
author_count_custom = Book.objects.annotate(
author_num=Count("author")
).values("name", "author_num")
print("【方法二】每本书作者数量(自定义键名):")
print(list(author_count_custom))
# =========================
# 【2】统计每个出版社最便宜的书的价格
# =========================
"""
# 这是我写的 需要优化
select a.id,b.name,a.max_price,a.min_price,a.sum_price from (
select p.id,max(b.price) as max_price,min(b.price) as min_price,sum(b.price) as sum_price from
publish p inner JOIN book b on p.id=b.publish_id GROUP BY p.id) a
left join publish b on a.id = b.id
SELECT
p.id AS publish_id,
p.name AS publish_name,
MIN(b.price) AS min_price,
MAX(b.price) AS max_price,
SUM(b.price) AS sum_price
FROM
publish p
LEFT JOIN
book b ON p.id = b.publish_id
GROUP BY
p.id;
"""
min_price_info = Publish.objects.annotate(
min_price=Min("book__price")
).values("name", "min_price")
print("每个出版社最便宜书的价格:")
print(list(min_price_info))
# 【3】统计 不止二个作者 的图书的信息
"""
# 我写的
select a.book_id,b.name,a.cnt from (
select book_id,COUNT(author_id) as cnt from book_author GROUP BY book_id
HAVING cnt >= 2 ) a
left join book b on a.book_id=b.id
# 不用 HAVING
SELECT b.id, b.name, a.author_num
FROM (
SELECT book_id, COUNT(author_id) AS author_num
FROM book_author
GROUP BY book_id
) a
JOIN book b ON a.book_id = b.id
WHERE a.author_num >= 2;
# 优化的 书名重复会出问题
SELECT b.id AS book_id, b.name, COUNT(ba.author_id) AS author_num
FROM book b
JOIN book_author ba ON b.id = ba.book_id
GROUP BY b.id, b.name
HAVING COUNT(ba.author_id) >= 2;
"""
# =========================
# 【3】统计作者数量大于 2 的图书
# =========================
books_multi_authors = Book.objects.annotate(
author_num=Count("author")
).filter(author_num__gt=2).values("name", "author_num")
print("作者数量大于2的图书信息:")
print(list(books_multi_authors))

12.9F查询
F() 表达式 = 引用数据库字段本身。不是 Python 变量,而是 SQL 层字段。
核心作用
- 字段之间比较:
Book.objects.filter(sale__gt=F("stock")) - 批量更新(核心):
Book.objects.update(price=F("price") + 500) - 字符串拼接:
Book.objects.update(name=Concat(F("name"), Value("_金榜")))
F() 核心总结
| 能力 | 说明 |
|---|---|
| 字段比较 | sale > stock |
| 数值运算 | price + 500 |
| 字符串拼接 | name + "_xxx" |
| 批量更新 | 一条 SQL 完成 |
Q() 本质用于构建复杂 WHERE 条件:
- AND
- OR
- NOT
示例
import os
import random
import django
# ========================================
# 【一】创建出版商和图书数据
# ========================================
# ----------------- 随机工具函数 -----------------
def get_random_phone():
first = "1" + str(random.randint(3, 9))
rest = "".join(str(random.randint(0, 9)) for _ in range(9))
return first + rest
def create_publish():
"""
创建出版商数据,如果已存在则跳过
"""
data = [("东方", "上海"), ("西方", "西安"), ("南方", "广州"), ("北方", "北京")]
for index, item in enumerate(data, start=1):
# 判断是否已存在
if Publish.objects.filter(name=f"{item}出版社").exists():
continue
else:
Publish.objects.create(
name=f"{item[0]}出版社",
addr=item[1],
phone=get_random_phone()
)
def create_book(book_name: str):
"""
创建图书数据,每个 book_name 创建 8 部
"""
create_publish()
for i in range(8):
book_full_name = f"{book_name}_{i + 1}部"
# 判断是否存在
if Book.objects.filter(name=book_full_name).exists():
continue
else:
# 随机选择一个出版商
publish_obj = Publish.objects.get(id=random.randint(1, 4))
Book.objects.create(
name=book_full_name,
price=random.randint(100, 300), # 价格示例
publish=publish_obj,
stock=random.randint(1, 10000),
sale=random.randint(1, 10000)
)
# ========================================
# 【二】更新库存和销售量
# ========================================
def update_sale_stock():
"""
随机增加每本书的 sale 和 stock
"""
book_all = Book.objects.all()
for book_obj in book_all:
book_obj.sale += random.randint(1, 10000)
book_obj.stock += random.randint(1, 10000)
book_obj.save()
# ========================================
# 【三】F() 查询示例
# ========================================
from django.db.models import F
def method_sale_gt_stock_one():
"""
方法一:使用 Python 遍历查询 sale > stock
"""
book_all_ = []
for book_obj in Book.objects.all():
if book_obj.sale > book_obj.stock:
print(f"sale: {book_obj.sale}, stock: {book_obj.stock}")
book_all_.append(book_obj)
print("\n使用 Python 遍历查询 sale > stock 的所有记录:")
for b in book_all_:
print(f"name: {b.name}, sale: {b.sale}, stock: {b.stock}")
def method_sale_gt_stock_two():
"""
方法二:使用 F() 查询 sale > stock
sale__gt=... 是 Django 查询条件语法:
sale → 模型字段
__gt → "greater than"(大于)
F("stock") → 数据库字段 stock 本身
整体意思:筛选 sale 字段的值 大于 同一条记录的 stock 字段的值
换成 SQL,大致等价于:
SELECT * FROM book WHERE sale > stock;
"""
result = Book.objects.filter(sale__gt=F("stock"))
print("\n使用 F() 查询 sale > stock 的所有记录:")
for book_obj in result:
print(f"name: {book_obj.name}, sale: {book_obj.sale}, stock: {book_obj.stock}")
def method_sale_up_500_one():
"""
方法一:遍历修改每本书价格 +500
"""
for book_obj in Book.objects.all():
book_obj.price = int(book_obj.price) + 500
book_obj.save()
print("\n遍历修改每本书价格 +500:")
for b in Book.objects.all():
print(f"name: {b.name}, sale: {b.sale}, stock: {b.stock}")
def method_sale_up_500_two():
"""
方法二:使用 F() 直接批量更新价格 +500
F("price"): F("price") 表示 引用当前记录的 price 字段值,不是取 Python 的值,而是 数据库字段本身
F("price") + 500: 表示 在数据库层面把 price 原来的值加 500
相当于 SQL:
UPDATE book SET price = price + 500;
"""
Book.objects.update(price=F("price") + 500)
for b in Book.objects.all():
print(f"name: {b.name}, sale: {b.sale}, stock: {b.stock}")
def add_info_name_one(data: str):
"""
方法一:前四本书名加前缀
"""
for book_obj in Book.objects.all()[:4]:
book_obj.name = f"{data}_{book_obj.name}"
book_obj.save()
for b in Book.objects.all():
print(f"name: {b.name}, sale: {b.sale}, stock: {b.stock}")
def add_info_name_two(data: str):
"""
方法二:使用 Concat 批量修改书名
F("name"): 引用 数据库中已有字段的值,而不是 Python 层的值
Value(f"_{data}"): 表示一个 固定值,可以是字符串或数字
Concat(F("name"), Value(f"_{data}")): 数据库函数,拼接字段和常量
"""
from django.db.models import Value
from django.db.models.functions import Concat
Book.objects.update(name=Concat(F("name"), Value(f"_{data}")))
for b in Book.objects.all():
print(f"name: {b.name}, sale: {b.sale}, stock: {b.stock}")
# ========================================
# 【四】Q() 查询示例
# ========================================
from django.db.models import Q
def get_sale_stock_method_one():
"""
Python 遍历查询 sale > 5000 and stock < 12000
"""
book_all_ = []
for book_obj in Book.objects.all():
if book_obj.sale > 5000 and book_obj.stock < 12000:
book_all_.append(book_obj)
print(book_all_)
# ========================================
# 【五】主程序入口
# ========================================
if __name__ == '__main__':
# 配置 Django 环境
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo09.settings')
django.setup()
# 导入模型
from user.models import Book, Publish
from scripts.utils import reset_auto_increment
# ----------------- 清空表 & 重置自增 -----------------
# Book.objects.all().delete()
# Publish.objects.all().delete()
# reset_auto_increment([Book, Publish])
# ----------------- 创建测试数据 -----------------
# create_book("朝花夕拾")
# create_book("骆驼祥子")
# create_book("荷塘月色")
# =====================
# F() 查询示例
# =====================
# method_sale_gt_stock_one() # Python 遍历
# method_sale_gt_stock_two() # F() 查询
# method_sale_up_500_one() # 遍历更新价格
# method_sale_up_500_two() # F() 更新价格
# add_info_name_one(data="爆款") # 遍历修改
# add_info_name_two(data="金榜") # Concat 批量修改
# =====================
# Q() 查询示例
# =====================
# 方式一:filter 中直接使用 Q()
# sale__gt=5000 → 销售量大于 5000
# stock__lt=12000 → 库存量小于 12000
# 默认多个 Q 对象作为参数传给 filter() 是 AND 关系
result = Book.objects.filter(Q(sale__gt=5000), Q(stock__lt=12000))
print("\n查询 销售量大于 5000 和 库存量小于 12000 的数据:")
for b in result:
print(f"name: {b.name}, sale: {b.sale}, stock: {b.stock}")
# 或条件
# sale__gt=5000 → 销售量大于 5000
# stock__lt=12000 → 库存量小于 12000
# | → OR 条件(满足任意一个即可)
result_or = Book.objects.filter(Q(sale__gt=5000) | Q(stock__lt=12000))
print("\n查询 销售量大于 5000 或者 库存量小于 12000 的数据:")
for b in result_or:
print(f"name: {b.name}, sale: {b.sale}, stock: {b.stock}")
# 方式二:创建 Q 对象
# 1.创建一个空 Q 对象
q = Q()
# 2.q.connector = "and"
# 指定内部条件的连接方式
# "and" → 所有条件都必须满足
# "or" → 满足任意条件即可
q.connector = "and" # "and" / "or"
# 3.添加第一个条件,等价于 sale > 5000
q.children.append(("sale__gt", 5000))
# 4.添加第二个条件,等价于stock < 12000
q.children.append(("stock__lt", 12000))
# 把这个 Q 对象作为 filter 条件,执行 SQL 查询
result_q = Book.objects.filter(q)
print("\n 使用 Q对象 拼接查询对象:")
for b in result_q:
print(f"name: {b.name}, sale: {b.sale}, stock: {b.stock}")
12.10Django事务
12.10.1简介
定义
事务 = 一组 SQL 操作,要么全部成功,要么全部失败
四大特性(ACID)
| 特性 | 说明 |
|---|---|
| A(Atomicity) | 原子性:要么全成功,要么全失败 |
| C(Consistency) | 一致性:数据必须合法 |
| I(Isolation) | 隔离性:事务之间互不干扰 |
| D(Durability) | 持久性:提交后永久生效 |
MySQL原生事务
START TRANSACTION; -- 开启事务
COMMIT; -- 提交
ROLLBACK; -- 回滚
Django事务本质
Django 事务本质就是对数据库事务的封装:
transaction.atomic() ≈ START TRANSACTION
正常结束 ≈ COMMIT
发生异常 ≈ ROLLBACK
12.10.2全局事务(ATOMIC_REQUESTS)
配置方式
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', # 数据库引擎
'NAME': 'django_demo', # 数据库名
'USER': 'root', # 数据库用户名
'PASSWORD': 'root', # 数据库密码
'HOST': '192.168.188.180', # 数据库主机
'PORT': '3306', # 数据库端口
"CHARSET": "utf8mb4", # 编码
# 开启全局事务的配置项 : 自动提交事务
"ATOMIC_REQUESTS": True
}
}

工作原理
每个请求自动包裹成一个事务:
请求开始 → 开启事务
请求结束 → 自动提交
发生异常 → 自动回滚
优点
- 不需要手动写事务
- 简单方便
缺点
- 性能开销大
- 所有请求都开启事务(不必要)
- 不适合高并发系统
12.10.3局部事务
装饰器
from django.db import transaction
@transaction.atomic
def my_view(request):
Book.objects.create(...)
特点
- 函数整体作为一个事务
- 出异常自动回滚
- 正常执行自动提交
示例
# 使用全局事务装饰器,确保该函数内的所有数据库操作要么全部成功,要么全部失败
@transaction.atomic
def have_transaction(request):
"""
在此函数内开启一个事务,所有数据库操作都会在同一事务内执行。
如果有任何异常发生,事务将被回滚,所有数据库操作都不会提交。
"""
# 创建一本新书
Book.objects.create(
name="have_transaction", # 书名
price="999", # 书价
publish=Publish.objects.get(id=1) # 关联出版商,假设id为1的出版商存在
)
# 故意抛出异常,触发事务回滚
int("a") # 这里会抛出 ValueError 异常
# 局部取消全局事务特性
@transaction.non_atomic_requests
def not_have_transaction(request):
"""
使用 @transaction.non_atomic_requests 装饰器取消局部事务特性
使得该函数内部的数据库操作不受全局事务控制
"""
# 创建一本新书,操作不会受到全局事务影响
Book.objects.create(
name="not_have_transaction", # 书名
price="999", # 书价
publish=Publish.objects.get(id=1) # 关联出版商,假设id为1的出版商存在
)
# 故意抛出异常,不会影响数据库操作的提交
int("a") # 这里会抛出 ValueError 异常,但不会回滚数据库事务
正常访问http://127.0.0.1:8000/have_transaction/不会有数据产生,因为有异常回滚事务
但是访问http://127.0.0.1:8000/not_have_transaction/已经取消了全局事务特性,就算代码有异常(数值转换失败),也依然会成功添加数据

12.10.4手动控制
执行流程
1. 开启事务
2. 创建保存点(savepoint)
3. 执行业务
4. 出错 → 回滚到保存点
5. 正常 → 提交
savepoint
| 操作 | 说明 |
|---|---|
| savepoint() | 创建回滚点 |
| rollback() | 回滚到该点 |
| commit() | 提交 |
优点
- 可以精细控制事务
- 支持部分回滚
示例
def have_transaction_one(request):
"""
使用手动事务管理,确保事务的回滚和提交。
在这个例子中,我们手动创建一个事务回滚点。
"""
# 使用 `with` 语句创建一个事务块,确保其中的操作要么全部成功,要么全部失败
with transaction.atomic():
# 创建一个事务回滚点,当前的操作点
sid = transaction.savepoint()
# 尝试执行数据库操作
try:
publish_obj, created = Publish.objects.get_or_create(
name="默认出版社",
defaults={
"addr": "北京",
"phone": "13800000000"
}
)
# 创建一本新书
Book.objects.create(
name="have_transaction_one", # 书名
price="999", # 书价
publish=Publish.objects.get(id=1) # 关联出版商,假设id为1的出版商存在
)
# 故意抛出异常,触发事务回滚
int("a") # 这里会抛出 ValueError 异常
# 如果发生异常,捕获并回滚事务
except Exception as e:
print(e) # 打印异常信息
# 回滚到事务回滚点,撤销已做的数据库操作
transaction.savepoint_rollback(sid)
# 如果没有异常,提交事务
else:
# 提交事务
transaction.savepoint_commit(sid)

12.10.5总结
| 事务类型 | 配置/写法 | 提交/回滚机制 | 优点 | 缺点 | 使用场景 |
|---|---|---|---|---|---|
| 全局事务(ATOMIC_REQUESTS) | DATABASES['default']['ATOMIC_REQUESTS'] = True |
Django 自动管理:请求结束 → 自动提交异常 → 自动回滚 | 简单,自动管理 | 性能差长事务易锁表不灵活 | 对全局请求都希望保证原子性的小型项目 |
| 局部事务(装饰器)(@transaction.atomic) | @transaction.atomic 装饰视图或函数 |
函数执行结束 → 提交异常 → 回滚 | 简单可控推荐方式 | 需要手动标注函数 | 核心业务逻辑:下单、支付、转账等 |
| 局部事务(手动控制)(with + savepoint) | python with transaction.atomic(): sid = transaction.savepoint() ... transaction.savepoint_commit(sid) |
通过 savepoint 控制回滚点异常 → 回滚到 savepoint正常 → commit | 灵活可精确回滚部分操作 | 代码稍复杂 | 多步骤业务逻辑、复杂事务嵌套、部分操作失败回滚 |
总结技巧:
-
全局事务 → 开箱即用,但性能差
-
装饰器事务 → 常用,推荐使用
-
手动事务 + savepoint → 高级用法,精确控制
12.11Django 模型(Model)字段类型及常用参数
12.11.1常用字段类型总结
| 字段类型 | 说明 | 示例 | 特点 |
|---|---|---|---|
AutoField |
自增整数列,通常作为主键 | id = models.AutoField(primary_key=True) |
自动生成 id 字段,如果模型没有主键会默认生成 |
IntegerField |
整数类型 | some_integer = models.IntegerField() |
-2147483648 到 2147483647 |
BigIntegerField |
长整型 | large_integer = models.BigIntegerField() |
-9223372036854775808 到 9223372036854775807 |
CharField |
字符串类型 | name = models.CharField(max_length=64) |
必须指定 max_length |
TextField |
长文本 | description = models.TextField() |
不限制长度 |
EmailField |
邮箱字符串 | email = models.EmailField(max_length=254) |
内部实际上是 varchar(254) |
DecimalField |
高精度小数 | price = models.DecimalField(max_digits=10, decimal_places=2) |
max_digits 总位数,decimal_places 小数位 |
BooleanField |
布尔类型 | is_active = models.BooleanField(default=True) |
数据库存 0/1 |
DateField / DateTimeField |
日期或日期时间 | created_at = models.DateTimeField(auto_now_add=True) |
auto_now_add 自动创建时间,auto_now 自动更新时间 |
FileField |
文件上传 | file = models.FileField(upload_to='files/') |
数据库存储文件路径,实际文件保存在 upload_to 指定目录 |
12.11.2关系型字段
| 字段类型 | 说明 | 示例 | 特点 |
|---|---|---|---|
ForeignKey |
一对多 | author = models.ForeignKey(Author, on_delete=models.CASCADE) |
on_delete 决定外键被删除时行为 (CASCADE, SET_NULL, …) |
OneToOneField |
一对一 | author = models.OneToOneField(Author, on_delete=models.CASCADE) |
常用于扩展已有表 |
ManyToManyField |
多对多 | authors = models.ManyToManyField(Author) |
书和作者多对多,Django 自动创建中间表管理关系 |
12.11.3字段常用参数
| 参数 | 说明 |
|---|---|
null=True |
数据库层允许为空(存储 NULL) |
blank=True |
Django admin/表单允许为空 |
unique=True |
字段值唯一 |
db_index=True |
创建索引,加快查询 |
default=value |
字段默认值 |
12.11.4自定义字段
你可以继承 models.Field 自定义字段,例如 FixCharField:
class FixCharField(models.Field):
def __init__(self, max_length, *args, **kwargs):
self.max_length = max_length
super().__init__(*args, **kwargs)
def db_type(self, connection):
return 'char(%s)' % self.max_length
12.11.5choices选择字段
PAY_METHODS = {1:"微信支付",2:"支付宝支付",3:"银行卡支付"}
pay_method = models.IntegerField(choices=[(1,"微信支付"),(2,"支付宝支付"),(3,"银行卡支付")])
12.11.6总结
示例
from django.db import models
# 【1】AutoField
# ● int 自增列,必须填入参数 primary_key=True。
# ● 当model中如果没有自增列,则自动会创建一个列名为id的列。
class ExampleAutoFieldModel(models.Model):
id = models.AutoField(primary_key=True) # 自增ID
# 【2】IntegerField
# ● 一个整数类型
# ● 范围在 -2147483648 to 2147483647。
class ExampleIntegerModel(models.Model):
some_integer = models.IntegerField() # 整数类型
# 【3】BigIntegerField (IntegerField)
# ● 长整型(有符号的)
# ● 范围在 -9223372036854775808 ~ 9223372036854775807
class ExampleBigIntegerModel(models.Model):
large_integer = models.BigIntegerField() # 长整型
# 【4】CharField
# ● 字符类型,必须提供max_length参数,表示字符长度
class ExampleCharModel(models.Model):
name = models.CharField(max_length=64) # 字符串类型,最大长度64
# 【5】EmailField
# ● varchar(254),适合存储邮箱地址
class ExampleEmailModel(models.Model):
email = models.EmailField(max_length=254) # 邮箱字段
# 【6】DecimalField
# ● max_digits,小数总长度
# ● decimal_places,小数位长度
class ExampleDecimalModel(models.Model):
price = models.DecimalField(max_digits=10, decimal_places=2) # 小数类型,总共最多10位,其中2位为小数
# 【7】TextField
# ● 文本类型,支持大段内容,无字数限制
class ExampleTextModel(models.Model):
description = models.TextField() # 长文本
# 【8】FileField
# ● 字符串,路径保存在数据库,文件上传到指定目录
# ● 参数:upload_to = "/data" 指定上传目录
class ExampleFileModel(models.Model):
file = models.FileField(upload_to='files/') # 文件上传字段
# 【9】BooleanField
# ● 布尔类型字段,数据库里面存储0/1
class ExampleBooleanModel(models.Model):
is_active = models.BooleanField(default=True) # 布尔字段
# 【10】DateField 和 DateTimeField
# ● DateField: 日期字段(格式:YYYY-MM-DD)
# ● DateTimeField: 日期时间字段(格式:YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ])
class ExampleDateTimeModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True) # 自动添加创建时间
updated_at = models.DateTimeField(auto_now=True) # 每次更新时自动更新
# 【11】ForeignKey
# ● 外键类型,通常设置在"一对多"的"多"方。
# ● 使用 on_delete 来定义删除外键时的行为。
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.ForeignKey(Author, on_delete=models.CASCADE) # 外键,删除作者时同时删除书籍
# 【12】OneToOneField
# ● 一对一字段,用来扩展已有的字段。
class AuthorDetail(models.Model):
author = models.OneToOneField(Author, on_delete=models.CASCADE) # 一对一外键关联
# 【补充】字段参数
# ● null:表示字段可以为空。
# ● unique:设置字段为唯一。
# ● db_index:为字段创建索引。
# ● default:设置字段的默认值。
# ● blank:Django admin中用于表示字段是否可以为空。
# 【13】自定义字段
# 在Django中没有固定的字符类型(如char),但我们可以自定义。
class FixCharField(models.Field):
'''
自定义char类型字段类
'''
def __init__(self, max_length, *args, **kwargs):
self.max_length = max_length
super().__init__(max_length=max_length, *args, **kwargs)
def db_type(self, connection):
''' 限定生成的数据库表字段类型char,长度为max_length指定的值 '''
return 'char(%s)' % self.max_length
# 使用自定义字符字段
class Class(models.Model):
id = models.AutoField(primary_key=True)
title = models.CharField(max_length=32)
class_name = FixCharField(max_length=16)
gender_choice = ((1, '男'), (2, '女'), (3, '保密'))
gender = models.SmallIntegerField(choices=gender_choice, default=3)
# 【14】选择字段 (choices)
# 例如支付方式字段,映射数字到支付方式。
PAY_METHODS = {
1: "微信支付",
2: "支付宝支付",
3: "银行卡支付",
}
class Payment(models.Model):
pay_method = models.IntegerField(choices=[
(1, "微信支付"),
(2, "支付宝支付"),
(3, "银行卡支付"),
])
def get_pay_method_display(self):
return self.get_pay_method_display() # 显示支付方式的名称

12.12Django批量插入
Django 的“批量插入”(bulk insert)本质是绕开 ORM 的逐条 INSERT,改为一次性生成一条或少量 SQL,把多条数据写入数据库,从而显著提升性能。这在数据初始化、日志导入、大规模写入场景中非常关键。
bulk_create 是 Django 提供的批量插入接口,通过 batch_size 控制分批写入,并支持 ignore_conflicts 和 update_conflicts 来处理唯一冲突,本质是将多条 INSERT 合并为少量 SQL,从而提升性能。
参数
| 参数名 | 默认值 | 作用说明 | 使用场景 | 注意事项 |
|---|---|---|---|---|
objs |
无 | 要插入的模型对象列表 | 所有批量插入 | 必须是同一模型实例列表 |
batch_size |
None |
控制每批插入数量(分批执行 SQL) | 大数据量(推荐必用) | 防止 SQL 过大、内存压力 |
ignore_conflicts |
False |
遇到唯一约束冲突时忽略错误 | 去重插入、导入数据 | 不会提示哪些数据被忽略 |
update_conflicts |
False |
冲突时执行更新(UPSERT) | 数据同步、覆盖导入 | 需配合下面两个参数 |
update_fields |
None |
冲突时要更新的字段 | UPSERT 场景 | 必须配合 update_conflicts |
unique_fields |
None |
冲突判断依据字段 | UPSERT 场景 | 必须是唯一索引字段 |
语法
QuerySet.bulk_create(
objs,
batch_size=None,
ignore_conflicts=False,
update_conflicts=False,
update_fields=None,
unique_fields=None
)
models.py
class Book(models.Model):
name = models.CharField(max_length=32)
price = models.CharField(max_length=32)
class Meta:
db_table = "book"
views.py
create方法
def insert_book_one(request):
start_time = time.time()
total = 10000
for i in range(total):
Book.objects.create(
name=f"水浒传_{i}部",
price=9.9 * i
)
if (i + 1) % 1000 == 0: # 每1000条打印一次
print(f"insert_book_one 进度: {i + 1}/{total}")
book_all = Book.objects.all()
print(f"insert_book_one 总耗时: {time.time() - start_time} s")
return render(request, "book_index.html", locals())

bulk_create方法
def insert_book_two(request):
start_time = time.time()
total = 10000
book_obj_list = []
for i in range(total):
book_obj = Book(
name=f"水浒传_{i}部",
price=9.9 * i
)
book_obj_list.append(book_obj)
if (i + 1) % 1000 == 0: # 每1000条打印一次生成进度
print(f"insert_book_two 生成对象进度: {i + 1}/{total}")
# 批量插入
book_all = Book.objects.bulk_create(book_obj_list)
print(f"insert_book_two 总耗时: {time.time() - start_time} s")
return render(request, "book_index.html", locals())

12.13Django内置序列化组件
定义
| 概念 | 说明 |
|---|---|
| 序列化(serialize) | Python对象 → 字符串(JSON / XML) |
| 反序列化(deserialize) | 字符串 → Python对象 |
12.13.1序列化(serialize)
serialize用于把 QuerySet 转成带 model 信息的 JSON 字符串
| 参数 | 说明 |
|---|---|
format |
序列化格式(json/xml/yaml/python) |
queryset |
要序列化的数据(必须是 QuerySet) |
fields |
指定字段 |
use_natural_foreign_keys |
外键用自然键 |
use_natural_primary_keys |
主键用自然键 |
serializers.serialize(format, queryset, **options)
示例
# ==============================================
# 【1】序列化 QuerySet
# ==============================================
def serialize_books():
books = Book.objects.all()[:10]
# JSON 序列化
json_data = serializers.serialize('json', books)
print("JSON 数据:\n", json_data)
# XML 序列化
xml_data = serializers.serialize('xml', books)
print("XML 数据:\n", xml_data)
# Python 原生对象(list of dict)
py_data = serializers.serialize('python', books)
print("Python 数据:\n", py_data)
# 指定字段序列化
json_fields = serializers.serialize('json', books, fields=('name', 'price'))
print("JSON 指定字段:\n", json_fields)

12.13.2反序列化(deserialize)
deserialize用于把这种 JSON 还原成 Django 模型对象
| 参数 | 说明 |
|---|---|
format |
数据格式 |
stream_or_string |
JSON/XML 字符串 |
ignorenonexistent |
忽略不存在字段 |
serializers.deserialize(format, stream_or_string, **options)
示例
# ==============================================
# 【2】反序列化
# ==============================================
def deserialize_books(json_data):
"""
将 JSON 数据反序列化并保存到数据库
"""
for obj in serializers.deserialize('json', json_data):
obj.save() # 保存到数据库

12.13.3视图返回
serializers.serialize 返回 JSON 字符串,而 JsonResponse 负责将 Python 对象转换为 JSON,因此不能直接嵌套使用,否则会导致双重序列化问题。
safe=False 是 JsonResponse 里的一个安全限制开关,核心作用非常明确:允许返回非 dict 类型的数据(通常是 list)。
# ==============================================
# 【3】Django 视图返回 JSON
# ==============================================
def books_json_view(request):
books = Book.objects.all()[:10]
data = serializers.serialize('json', books)
# safe=False 是因为返回的是 list 而不是 dict
return JsonResponse(data, safe=False)

12.14Django分页器
Django 内置分页工具:
from django.core.paginator import Paginator
作用:将一个大的 QuerySet 拆分成多个“页(Page)”返回
三个核心对象
| 对象 | 作用 |
|---|---|
| Paginator | 分页器(总控) |
| Page | 某一页的数据 |
| PageNumber | 当前第几页 |
分页会用就行,用多了就懂了
paginations.py
class Pagination(object):
def __init__(self, current_page, all_count, per_page_num=2, pager_count=11):
"""
封装分页相关数据
:param current_page: 当前页
:param all_count: 数据库中的数据总条数
:param per_page_num: 每页显示的数据条数
:param pager_count: 最多显示的页码个数
"""
try:
current_page = int(current_page)
except Exception as e:
current_page = 1
if current_page < 1:
current_page = 1
self.current_page = current_page
self.all_count = all_count
self.per_page_num = per_page_num
# 总页码
all_pager, tmp = divmod(all_count, per_page_num)
if tmp:
all_pager += 1
self.all_pager = all_pager
self.pager_count = pager_count
self.pager_count_half = int((pager_count - 1) / 2)
@property
def start(self):
return (self.current_page - 1) * self.per_page_num
@property
def end(self):
return self.current_page * self.per_page_num
def page_html(self):
# 如果总页码 < 11个:
if self.all_pager <= self.pager_count:
pager_start = 1
pager_end = self.all_pager + 1
# 总页码 > 11
else:
# 当前页如果<=页面上最多显示11/2个页码
if self.current_page <= self.pager_count_half:
pager_start = 1
pager_end = self.pager_count + 1
# 当前页大于5
else:
# 页码翻到最后
if (self.current_page + self.pager_count_half) > self.all_pager:
pager_end = self.all_pager + 1
pager_start = self.all_pager - self.pager_count + 1
else:
pager_start = self.current_page - self.pager_count_half
pager_end = self.current_page + self.pager_count_half + 1
page_html_list = []
# 添加前面的nav和ul标签
page_html_list.append('''
<nav aria-label='Page navigation>'
<ul class='pagination'>
''')
first_page = '<li><a href="?page=%s">首页</a></li>' % (1)
page_html_list.append(first_page)
if self.current_page <= 1:
prev_page = '<li class="disabled"><a href="#">上一页</a></li>'
else:
prev_page = '<li><a href="?page=%s">上一页</a></li>' % (self.current_page - 1,)
page_html_list.append(prev_page)
for i in range(pager_start, pager_end):
if i == self.current_page:
temp = '<li class="active"><a href="?page=%s">%s</a></li>' % (i, i,)
else:
temp = '<li><a href="?page=%s">%s</a></li>' % (i, i,)
page_html_list.append(temp)
if self.current_page >= self.all_pager:
next_page = '<li class="disabled"><a href="#">下一页</a></li>'
else:
next_page = '<li><a href="?page=%s">下一页</a></li>' % (self.current_page + 1,)
page_html_list.append(next_page)
last_page = '<li><a href="?page=%s">尾页</a></li>' % (self.all_pager,)
page_html_list.append(last_page)
# 尾部添加标签
page_html_list.append('''
</nav>
</ul>
''')
return ''.join(page_html_list)
views.py
from .paginations import Pagination
def book_page(request):
# 当前页 current_page 从当前的 请求地址中获取
# 起始页(起始索引) start_page
# 结束页(结束索引) end_page
# 每一页的条数 per_page_num
# 【一】查询所有数据
book_all = Book.objects.all()
# 【二】获取当前页码
current_page = request.GET.get("page")
# 【三】交给分页器类分页
page_obj = Pagination(current_page=current_page,
all_count=book_all.count(),
per_page_num=20)
# 【四】调用分页器对象分页数据
query_set = book_all[page_obj.start:page_obj.end]
return render(request, "book_page.html", locals())
book_page.html
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="{% static 'js/jquery.min.js' %}"></script>
<script src="{% static 'plugins/bootstrap/js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/sweetalert.min.js' %}"></script>
<link rel="stylesheet" href="{% static 'plugins/bootstrap/css/bootstrap.css' %}">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
{% for book in query_set %}
<p>{{ book.name }} </p>
{% endfor %}
{{ page_obj.page_html|safe }}
</div>
</div>
</div>
</body>
</html>

12.15Django的form组件
12.15.1简介
Django Form = 表单生成 + 数据校验 + 数据清洗 + 错误管理。本质是一个“数据校验与转换层”。
作用
- 生成 HTML 表单(减少前端工作)
- 校验用户输入(合法性、安全性)
- 清洗数据 → cleaned_data(标准化)
- 返回错误信息 → errors
核心思想:前端只负责展示,后端负责校验(更安全)
Form vs ModelForm
- Form
- 手动定义字段
- 不依赖数据库
- ModelForm
- 自动根据模型生成字段
- 直接操作数据库
执行流程
1. 用户访问页面 → GET
2. 创建空表单对象 form_obj = Form()
3. 渲染 HTML
4. 用户提交 → POST
5. form_obj = Form(request.POST)
6. form_obj.is_valid() 触发校验
7. 校验成功:
form_obj.cleaned_data
8. 校验失败:
form_obj.errors
流程图:
请求 → Form实例化 → is_valid → clean → cleaned_data/errors
12.15.2Field参数详解
12.15.2.1通用参数
| 参数 | 类型 | 默认值 | 作用 | 备注 |
|---|---|---|---|---|
required |
bool | True | 是否必填 | False 可为空 |
label |
str | 字段名 | 表单标签 | 前端显示 |
initial |
任意 | None | 默认值 | 仅展示用 |
help_text |
str | None | 提示信息 | 辅助说明 |
error_messages |
dict | 内置 | 自定义错误信息 | 可细粒度控制 |
validators |
list | [] | 自定义校验规则 | 函数列表 |
widget |
Widget | 默认组件 | 控制HTML样式 | ⭐核心 |
disabled |
bool | False | 是否禁用 | 前端不可修改 |
localize |
bool | False | 本地化格式 | 时间/数字 |
label_suffix |
str | ":" | 标签后缀 | 如:用户名: |
12.15.2.2字符串字段(CharField)
| 参数 | 类型 | 作用 | 备注 |
|---|---|---|---|
max_length |
int | 最大长度 | 必填常用 |
min_length |
int | 最小长度 | 登录常用 |
strip |
bool | 去除首尾空格 | 默认True |
12.15.2.3数值字段
| 字段 | 说明 |
|---|---|
IntegerField |
整数 |
FloatField |
浮点数 |
DecimalField |
精确小数(推荐金额) |
12.15.2.4时间字段
| 字段 | 说明 |
|---|---|
DateField |
日期 |
TimeField |
时间 |
DateTimeField |
日期时间 |
12.15.2.5特殊字段
| 字段 | 作用 |
|---|---|
EmailField |
邮箱格式校验 |
URLField |
URL格式校验 |
RegexField |
正则校验 |
FileField |
文件上传 |
ImageField |
图片上传(需 Pillow) |
12.15.2.6选择类字段
| 字段 | 说明 |
|---|---|
ChoiceField |
单选(静态) |
MultipleChoiceField |
多选 |
ModelChoiceField |
单选(数据库) |
ModelMultipleChoiceField |
多选(数据库) |
12.15.3组件(Widget)
| Widget | 生成HTML | 说明 |
|---|---|---|
TextInput |
<input type="text"> |
文本框 |
PasswordInput |
<input type="password"> |
密码框 |
RadioSelect |
<input type="radio"> |
单选 |
CheckboxInput |
<input type="checkbox"> |
单个复选 |
CheckboxSelectMultiple |
多个 checkbox | 多选 |
Select |
<select> |
下拉框 |
SelectMultiple |
多选下拉 | 多选 |
Django Form 中 Field 负责数据校验规则,Widget 负责 HTML 展示形式,两者配合实现完整的表单处理。
12.15.4基础使用
定义form
from django.core.validators import RegexValidator
# 创建组件并添加额外的参数
class RegisterForm(forms.Form):
username = forms.CharField(
# 当前输入的最大字符长度
max_length=8,
# 当前输入的最小长度
min_length=3,
# 当前参数是否是必须输入的参数 默认是必须传的参数
required=True,
# 为 当前的标签设置初始值
initial="peng",
# 给当前 label 标签中的提示文本修改
# 如果改了就是自定义的值 没改就是当前的字段名
label="用户名",
# 给当前的 label 标签提示文字后面的标识 默认是 :
label_suffix=": ",
# 给当前字段添加的额外的组件
# 比如增加的 class 值 style 值
# 向让当前输入框是一个 密码输入框 / 单选框 / 多选框
widget="",
# 给当前标签的注释文本
help_text="这是用户名的输入框",
# 如果当前字段 中的校验出校错误 可以定制错误的信息
error_messages={
"required": "用户名必须传!",
"max_length": "当前用户名的长度最大是 8 位!"
},
# 是否使用当前本地化 在美国用 en-us 中国 zn-hans
localize=True,
# 在前端是否禁用当前标签的输入
disabled=True
)
# 密码的标签 -- PasswordInput
password = forms.CharField(max_length=6, min_length=2,
# 当前组件类型是 password的输入框
# 给当前标签增加额外的属性名和属性值
widget=forms.PasswordInput(
attrs={
"class": "form-control"
}
), )
# radio 单选框
gender = forms.ChoiceField(
# 当前单选框的备选项
choices=((0, "女"), (1, "男")),
# 当前提示内容
label="性别",
# 当前的默认值
initial=0,
# # 当前组件类型是 RadioSelect 单选框
widget=forms.RadioSelect()
)
# select 下拉单选框
# SelectMultiple 下拉多选框
# Select 下拉单选框
hobby_ball = forms.MultipleChoiceField(
choices=((1, "篮球"), (2, "足球"), (3, "双色球"),),
label="爱好",
initial=[1, 3],
# widget=forms.widgets.SelectMultiple()
widget=forms.widgets.Select()
)
# 多选矿
keep = forms.ChoiceField(
label="是否记住密码",
# 默认勾选记住密码
initial="checked",
widget=forms.widgets.CheckboxInput()
)
hobby = forms.MultipleChoiceField(
choices=((1, "唱"), (2, "跳"), (3, "rap"),),
label="爱好",
initial=[1, 3],
widget=forms.widgets.CheckboxSelectMultiple()
)
# 某一天你觉的上面的校验规则不好
# 校验当前的电话是 符合 11 位 并且是符合国内的
# 自定义校验器
# 前端没有变化 ====> 校验器主要体现在 is_valid 触发校验 按照正则 匹配 如果符合 就通过 不符合就给提示信息
phone = forms.CharField(
validators=[RegexValidator(
r'^1\d{10}$|^(0\d{2,3}-?|\(0\d{2,3}\))?[1-9]\d{4,7}(-\d{1,8})?$', '请输入电话'
)])
# 邮箱的标签
email = forms.EmailField()
views.py
# Create your views here.
class RegisterView(View):
def get(self, request, *args, **kwargs):
# 第一步导入自己创建的组件类 RegisterForm
# 第二步创建一个 form 组件的对象
form_obj = RegisterForm()
return render(request, "register.html", locals())
def post(self, request, *args, **kwargs):
data = request.POST
print(data)
# data.update(age=18)
data = {'password': 'dsadds', 'email': 'sada@qq.com', "age": 18, "phone": "1669696"}
# 我们可以借助 form 组件的对象 内置的校验规则帮助我们校验数据
# 第一步 直接将获取到 QueryDict 对象传给 RegisterForm 类 得到一个 form 组件的对象
form_obj = RegisterForm(data)
# 第二步 触发 RegisterForm 组件的校验
# 【1】触发当前 RegisterForm 组件的校验并获取校验后的标志结果
print(form_obj.is_valid()) # True ---> 当前传入的数据是符合 RegisterForm 组件的校验规则的
# 【2】查看已经校验后的合法的数据 , 查看到的是全部符合的数据 如果数据不符合就会被抛弃
print(form_obj.cleaned_data)
# {'password': 'dsadds', 'email': 'sada@qq.com'}
# 【3】查看校验过的数据的字段
print(form_obj.changed_data)
# ['username', 'password', 'email']
# 【4】查看当前校验失败的错误信息
print(form_obj.errors)
# <ul class="errorlist"><li>username<ul class="errorlist"><li>Ensure this value has at least 3 characters (it has 2).</li></ul></li></ul>
# 【5】如果额外传入多的数据则会被直接舍弃掉不会保存也不会校验
# 【6】如果传入的数据少则会被直接有错误信息说你某个字段没传
if not form_obj.is_valid():
print(f"不符合")
# 符合正常的数据
return JsonResponse({"success": form_obj.is_valid()})
register.html
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="{% static 'js/jquery.min.js' %}"></script>
<link href="{% static 'plugins/bootstrap/css/bootstrap.css' %}" rel="stylesheet">
<script src="{% static 'plugins/bootstrap/js/bootstrap.min.js' %}"></script>
</head>
<body>
<form action="" method="post" novalidate>
{% for form in form_obj %}
{# 遍历出来的对象只是 input 标签 #}
<p>
{# 给当前 input 标签创建的 label 标签 #}
{{ form.label_tag }}
<br>
{# name 就是当前的 form 组件中的字段名 #}
{{ form.name }}
<br>
{# 在 form 组件字段上定义的 label 内容 #}
{{ form.label }}
<br>
{# 获取到当前的 form 对象校验后的错误信息 #}
{{ form.errors }}
<br>
{# 当前 input 标签上的id值 #}
{{ form.id_for_label }}
<br>
{# 当前 input 标签上的id值 #}
{{ form.auto_id }}
<br>
{# 在 form 组件字段上 定义的 help_text 的值 #}
{{ form.help_text }}
<br>
{{ form }}</p>
{% endfor %}
<p>
<input type="submit">
</p>
</form>
</body>
</html>

12.15.5校验钩子函数(clean)
局部钩子(clean_<field>)
- 只负责:单个字段校验
- 触发时机:字段基础校验之后
- 返回值:当前字段的值
全局钩子(clean)
- 负责:多个字段之间的逻辑关系
- 触发时机:所有字段都校验完之后
- 返回值:整个 cleaned_data
forms.py
class LoginForm(forms.Form):
"""
登录/注册表单示例
演示了:
- 字段定义
- 局部钩子 clean_<field>()
- 全局钩子 clean()
"""
# ----------------------------
# 用户名字段
# ----------------------------
username = forms.CharField(
required=False, # 是否必填(False 表示可为空)
max_length=8, # 最大长度
min_length=3, # 最小长度
label="用户名", # 表单标签
label_suffix=":", # 标签后缀
initial="peng",
widget=forms.TextInput(attrs={ # 指定 HTML 渲染样式
"class": "from-control" # CSS 类名
})
)
# ----------------------------
# 密码字段
# ----------------------------
"""
forms.PasswordInput 默认 不渲染 initial 或 value 属性,这是出于安全考虑:
浏览器渲染 <input type="password"> 时,如果直接显示明文密码,存在泄露风险
所以 Django 的 PasswordInput 在渲染 HTML 时会忽略 initial
"""
password = forms.CharField(
required=False,
max_length=8,
min_length=3,
label="密码",
label_suffix=":",
# initial="123qwe",
widget=forms.PasswordInput(
attrs={
"class": "from-control",
"value": "123qweasdzxc" # 强制渲染初始值
}
)
)
# ----------------------------
# 确认密码字段
# ----------------------------
confirm_password = forms.CharField(
required=False,
max_length=8,
min_length=3,
label="确认密码",
label_suffix=":",
# initial="qwe",
widget=forms.PasswordInput(
attrs={
"class": "from-control",
"value": "123qwe" # 强制渲染初始值
}
)
)
# ========================================
# 局部钩子:校验单个字段
# ========================================
def clean_username(self):
"""
局部钩子:对 username 字段进行额外校验
说明:
- 每个字段都可以定义 clean_<field>() 方法
- 局部钩子只作用于当前字段
- cleaned_data 中只包含当前字段的值
"""
# 获取当前字段清理后的数据
username = self.cleaned_data.get("username")
# 校验用户名是否以 nb_ 开头
if not username.startswith("nb_"):
# 给当前字段添加错误信息
# 这样 form.is_valid() 会返回 False
self.add_error("username", "不是以 nb_ 开头")
# 返回字段原始值(必须返回,否则会被置为 None)
return self.cleaned_data.get("username")
# ========================================
# 全局钩子:跨字段校验
# ========================================
def clean(self):
"""
全局钩子:可以校验多个字段的逻辑关系
典型场景:两次密码是否一致
注意:
- 必须返回 cleaned_data,否则数据不会保留
- 可以使用 add_error 给指定字段添加错误信息
"""
# 获取所有字段清理后的数据
password = self.cleaned_data.get("password")
confirm_password = self.cleaned_data.get("confirm_password")
# 校验密码是否一致
if password != confirm_password:
# 给 confirm_password 字段增加错误提示
self.add_error("confirm_password", "两次密码不一致!")
# 返回 cleaned_data,保留数据用于视图或保存
return self.cleaned_data

views.py
class LoginView(View):
def get(self, request, *args, **kwargs):
# 第一步导入自己创建的组件类 RegisterForm
# 第二步创建一个 form 组件的对象
form_obj = LoginForm()
return render(request, "login.html", locals())
def post(self, request, *args, **kwargs):
form_obj = LoginForm(request.POST)
if not form_obj.is_valid():
print(form_obj.errors)
else:
print(form_obj.cleaned_data)
# 进行模型表数据的新增
return JsonResponse({"success": form_obj.is_valid(), "msg": form_obj.errors, "data": form_obj.cleaned_data},
json_dumps_params={"ensure_ascii": False})

代码:
- 模拟
username和password长度错误 - 局部钩子
clean_username验证username必须nb_开头 - 全局钩子
clean验证password和confirm_password必须一致

执行流程
form.is_valid()
↓
is_valid()
↓
full_clean()
↓
_clean_fields() ← 字段级
↓
_clean_form() ← 表单级
↓
_post_clean() ← ModelForm 用
12.15.6ModelForm
ModelForm 是 Django Form 的升级版,它可以直接与模型(Model)搭配使用,自动生成表单字段,并且绑定模型的字段验证逻辑。
优点:
-
减少重复代码:无需重复定义字段类型、校验规则
-
自动生成表单字段 label、help_text
-
可与模型实例绑定,实现新增、修改数据操作
-
支持局部和全局钩子函数,触发方式与 Form 一致
Meta 参数总结
| 参数 | 类型 | 作用 | 备注 |
|---|---|---|---|
model |
Model | 绑定模型 | 必须 |
fields |
list / "all" | 显示字段 | 推荐 |
exclude |
list | 排除字段 | 和 fields 二选一 |
labels |
dict | 字段标签 | 前端显示 |
help_texts |
dict | 提示信息 | 用户提示 |
error_messages |
dict | 错误信息 | 自定义报错 |
widgets |
dict | 控件类型 | 改HTML样式 |
models.py
# Create your models here.
class User(models.Model):
username = models.CharField(max_length=32)
password = models.CharField(max_length=32)
confirm_password = models.CharField(max_length=32)
forms.py
注意Meta的配置
# ==============================
# 【注册表单组件】继承自 ModelForm
# ModelForm 是基于模型的表单,可以直接将模型字段映射到表单字段
# ==============================
class UserModelForm(forms.ModelForm):
"""
注册表单:
- 支持自定义字段规则
- 支持局部字段校验(单字段)
- 支持全局表单校验(多个字段)
"""
# ------------------------------
# 自定义字段:username
# 如果模型里有同名字段,这里定义的会覆盖模型默认字段的渲染规则
# ------------------------------
username = forms.CharField(
required=False, # 表示该字段可以不填,False 表示非必填
max_length=8, # 最大长度 8 个字符
min_length=3, # 最小长度 3 个字符
label="用户名", # 在模板中显示的字段名称
label_suffix=":", # label 后缀
initial="123qweasdzxc", # 为 当前的标签设置初始值
widget=forms.TextInput(attrs={
"class": "from-control" # 给输入框添加 CSS 类
})
)
# ------------------------------
# Meta 配置类
# ------------------------------
class Meta:
# 绑定表单的模型
model = User
# 指定表单需要渲染哪些字段
# fields = "__all__" # 如果使用 "__all__" 会包含模型中所有字段
fields = ["username", "password"] # 只渲染用户名和密码字段
# 排除不需要显示的字段
exclude = ["password"] # 这里排除 password,注意:同时指定 fields 和 exclude 通常不推荐,会报警告
# 字段本地化设置,默认不启用
localized_fields = [] # 可以对日期、时间字段使用本地化显示
# ===============================
# 【局部钩子】只对 username 字段进行校验
# ===============================
def clean_username(self):
"""
局部钩子:
- clean_<字段名> 是 Django 的约定
- 只对当前字段做校验
- 这里要求用户名必须以 "nb_" 开头
"""
# cleaned_data 只包含经过初步验证后的字段
username = self.cleaned_data.get("username")
if not username.startswith("nb_"):
# add_error 可以在表单上为某个字段添加错误信息
self.add_error("username", f"不是以 nb_ 开头 ")
# 局部钩子必须返回最终值,否则会影响表单数据
return self.cleaned_data.get("username")
# ===============================
# 【全局钩子】对整个表单的数据进行校验
# ===============================
def clean(self):
"""
全局钩子:
- clean() 用于对多个字段之间的关联逻辑进行校验
- 这里校验 password 和 confirm_password 是否一致
"""
# 获取所有清理过的数据
cleaned_data = super().clean() # 推荐调用父类 clean 方法
password = cleaned_data.get("password")
confirm_password = cleaned_data.get("confirm_password")
# 校验两次密码是否一致
if password != confirm_password:
# add_error 会把错误绑定到 confirm_password 字段上
self.add_error("confirm_password", "两次密码不一致!")
# 返回清理后的数据,必须返回
return cleaned_data

views.py
Django 表单校验遵循“字段校验优先”的原则,只有字段基础校验通过后,才会执行 clean_<字段>() 和 clean() 方法。所以这里先验证max_length
class UserModelView(View):
def get(self, request, *args, **kwargs):
# 第一步导入自己创建的组件类 RegisterForm
# 第二步创建一个 form 组件的对象
form_obj = UserModelForm()
return render(request, "login.html", locals())
def post(self, request, *args, **kwargs):
form_obj = UserModelForm(request.POST)
if not form_obj.is_valid():
print(form_obj.errors)
else:
print(form_obj.cleaned_data)
return JsonResponse({"success": form_obj.is_valid(), "msg": form_obj.errors, "data": form_obj.cleaned_data},
json_dumps_params={"ensure_ascii": False})

13.Django中Cookie和Session
13.1Cookie
13.1.1简介
在 Django 中,Cookie 是一种小型的文本文件,用于存储客户端的数据,通常用于在用户和服务器之间存储会话状态信息。Django 提供了用于处理 Cookie 的内置功能。
Cookie 基础
Cookie 是由键值对组成的,每个键值对都会存储在浏览器中,通常包含以下信息:
- 键:Cookie 的名字(如
user_id)。 - 值:与键对应的内容(如用户的 ID 或令牌)。
- 过期时间:Cookie 何时过期,默认为会话结束时过期(即浏览器关闭时)。
- 路径:指定哪些 URL 路径能访问此 Cookie,默认为根路径
/。 - 域:指定哪些域可以访问 Cookie。
- 安全标志(Secure):如果设置了此标志,Cookie 只有在 HTTPS 协议下才能传输。
- HttpOnly 标志:如果设置了此标志,JavaScript 无法访问此 Cookie,这有助于防止跨站脚本攻击(XSS)。
特点
- 存在客户端(浏览器)
- 键值对结构
- 自动携带请求
- 有大小限制(约4KB)
缺点
- 不安全(可被篡改),易被窃取
- 容量小
Cookie 工作流程
1️⃣ 用户登录
↓
2️⃣ 服务端验证成功
↓
3️⃣ set_cookie 写入浏览器
↓
4️⃣ 浏览器保存 cookie
↓
5️⃣ 后续请求自动携带 cookie
↓
6️⃣ 服务端识别用户身份
总结
Cookie 是服务器写入浏览器的一段键值对数据,用于维持用户登录状态,每次请求会自动携带。
13.1.2设置Cookie(set_cookie)
set_cookie
- name:Cookie 的名字
- value:Cookie 的值
- max_age:设置 Cookie 的有效期,单位是秒。如果设置了该参数,表示 Cookie 会在给定的时间后过期。
- expires:也可以使用
expires来设置过期时间,可以是一个日期时间对象。 - path:设置路径,限制 Cookie 的访问范围,默认为
/。 - domain:限制 Cookie 访问的域。
- secure:如果设置为
True,则表示只有在 HTTPS 环境下才会传输该 Cookie。 - httponly:如果设置为
True,则 JavaScript 无法访问该 Cookie。
# cookie
obj = HttpResponse(f"{username} 登陆成功!")
# 在浏览器中保存 cookie
# key = sign,value = username
"""
set_cookie 作用:
向浏览器响应头写入 cookie,实现状态保存
"""
# ---------------------------------------------------------
# key:cookie 名称
# value:cookie 值
# max_age:存活时间(秒)
# expires:过期时间(datetime 或时间点)
# path:cookie 生效路径
# domain:cookie 生效域名
# secure:是否只允许 HTTPS
# httponly:是否禁止 JS 访问(防 XSS)
# samesite:防 CSRF(Lax / Strict / None)
# ---------------------------------------------------------
# 标准 datetime 写法 过期时间:当前时间 + 1分钟
# expire_time = datetime.now() + datetime.timedelta(minutes=1)
# obj.set_cookie(
# "sign",
# username,
# expires=expire_time
# )
# Django支持的秒数方式 一分钟过期
obj.set_cookie("sign", username, expires=60)
13.1.3获取Cookie(get)
Django 会自动将请求中的 Cookie 数据解析为一个字典,存储在 request.COOKIES 中。可以通过 Cookie 的名字来获取对应的值:
def index_one(request):
print(request.COOKIES)
sign = request.COOKIES.get("sign")
if sign == "peng":
return HttpResponse("index_one")
else:
return HttpResponse("未登录请先登陆")
13.1.4删除Cookie(delete_cookie)
# 【1】创建响应对象
# HttpResponse:用于返回给浏览器的响应
# 注意:Cookie 的操作必须基于 response 对象
obj = HttpResponse("退出成功!")
# 【2】删除 Cookie(客户端)
# delete_cookie:
# 作用:让浏览器删除指定 key 的 Cookie
#
# sign:
# 一般用于存储登录标识(用户名 / token)
#
# 本质:
# 在响应头中设置:
# Set-Cookie: sign=; Max-Age=0
obj.delete_cookie("sign")
13.1.5总结
在 Django 中,Cookie 用于存储客户端数据,并通过 request.COOKIES 进行访问。它可以用来保存会话信息、用户偏好等。虽然 Cookie 在客户端存储,但 Django 提供了一些工具来管理它们的安全性和生命周期。Session 通常用于处理更复杂的会话数据,存储在服务器端,安全性较高。
13.2Session
13.2.1简介
在 Django 中,Session 是一种在服务器端存储用户会话信息的机制,它用于在多次请求之间维持客户端的状态(例如用户登录状态、购物车信息等)。与 Cookie 不同,Session 的数据存储在服务器端,客户端仅通过一个唯一的 Session ID 来标识当前会话。
Session的工作原理
-
用户请求:用户向服务器发送请求,Django 通过会话系统生成一个唯一的
sessionid。 -
Session ID:Django 会将
sessionid存储在用户的浏览器 Cookie 中。默认情况下,Django 会自动在每次请求时将sessionid发送回服务器。 -
Session 数据存储:服务器端会根据
sessionid查找或创建一个 Session 对象。Session 对象存储在服务器端(通常在数据库中或内存中),而不是客户端。 -
Session 数据访问:开发者可以通过
request.session来读取和修改与用户会话相关的数据。
Session的工作流程
1. 用户登录后提交用户名 + 密码
2. 服务端对数据进行加密处理
3. 将加密后的 session 数据存入数据库(django_session 表)
4. 返回 sessionid 给浏览器(Cookie 中保存)
5. 下次请求携带 sessionid,服务端自动解析还原数据
13.2.2Django的Session 依赖数据库
使用 session 需要:
-
1.配置
settings.py\INSTALLED_APPS中的django.contrib.sessions -
2.执行数据库迁移:
python manage.py migrate -
3.系统会自动创建表:
django_session
配置 django.contrib.sessions

执行数据库迁移,我这里通过manage.py直接执行的
makemigrations
migrate

django_session表

13.2.3Session存储方式
Django 支持多种方式存储 Session 数据,可以通过 settings.py 中的 SESSION_ENGINE 配置项来指定使用哪种存储方式。常见的存储方式包括:
数据库存储:使用数据库存储 Session 数据。需要执行 python manage.py migrate 来创建 Session 数据表。
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
缓存存储:使用缓存(如 Memcached、Redis)来存储 Session 数据。
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
文件存储:将 Session 数据存储在服务器上的文件系统中。
SESSION_ENGINE = 'django.contrib.sessions.backends.file'
本地内存存储:默认存储在内存中,仅在开发环境中使用。
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
13.2.4设置Session
通过 request.session 可以存取 Session 数据。它是一个字典,允许你设置、获取、删除键值对
# session
# Django 会:
# - 对数据进行签名/加密
# - 写入 django_session 表
# - 返回 sessionid 给浏览器
request.session["sign"] = username + password
# 设置 session 过期时间
# 0:关闭浏览器即失效
request.session.set_expiry(0)
13.2.5获取Session
通过 request.session 来访问存储在 Session 中的数据。如果某个键不存在,返回 None,可以使用 get() 方法指定默认值
def index_two(request):
# 从请求中获取当前的 session
# 从前端获取到当前传入 的 sessionid ---> 去数据库中取 加密串的数据 ---> 反解 ---> 解除加密的时候放进去的值
sign = request.session.get("sign")
print(sign) # peng666
if sign == "peng666":
return HttpResponse("index_two")
else:
return HttpResponse("未登录请先登陆")
13.2.6删除Session
总结
- 彻底清除 session:使用
flush(),它会清除 session 数据并生成新的 session ID。 - 清空 session 数据但保持 session ID:使用
clear(),它会清空所有数据,但保持原 session ID。 - 删除单个 session 数据项:使用
del request.session['key_name']。 - 不推荐使用
delete(),除非你在使用自定义 session 后端并且明确知道该方法的实现。
request.session.delete()
del request.session['key_name']
request.session.clear()
request.session.flush()
| 方法名称 | 功能描述 | 使用场景 | 是否删除 session ID / 生成新 ID |
|---|---|---|---|
flush() |
清除当前 session 数据,删除 session ID(即删除客户端的 session_id cookie),重新生成一个新的 session_key。 | 用于彻底注销用户、清除所有 session 数据,并且清除 session ID(例如用户退出登录)。 | 删除 session ID,并生成新的 session ID |
clear() |
清除当前 session 中的所有数据,但保持 session ID。 | 用于清空 session 数据(例如清空购物车、用户信息),但保持原有的 session ID。 | 不删除 session ID,不生成新 ID |
del |
删除指定的 session 数据(类似字典中的 del 操作)。 |
用于删除 session 中的特定键值对。例如用户登出时清除某个特定的数据。 | 不影响 session ID |
delete() |
(不推荐使用) Django 默认没有 delete() 方法,但如果使用自定义的 session 后端,可能会有此方法。 |
不推荐使用。如果使用了自定义的 session 后端,可能会支持删除操作。 | 不影响 session ID |
13.2.7总结
Django 使用 Session 来管理和存储服务器端的会话数据。
Session 数据通过 request.session 存储和访问,Django 会自动为每个用户生成一个唯一的 sessionid。
Session 数据可以存储在不同的存储后端(如数据库、缓存或文件系统中)。
会话标识符(sessionid)通过 Cookie 存储在客户端,服务器通过该 ID 访问和更新 Session 数据。
Django 提供了多种配置选项来管理 Session 数据的过期、删除和安全性。
13.3Cookie和Session的示例
login.html
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="{% static 'js/jquery.min.js' %}"></script>
<link href="{% static 'plugins/bootstrap/css/bootstrap.min.css' %}" rel="stylesheet">
<script src="{% static 'plugins/bootstrap/js/bootstrap.min.js' %}"></script>
</head>
<body>
<form action="" method="post">
<p>
username : <input type="text" name="username" value="peng">
</p>
<p>
password : <input type="text" name="password" value="666">
</p>
<p>
<input type="submit">
</p>
</form>
</body>
</html>
views.py
def write_data(data, path="user_data.json"):
"""
将数据写入 JSON 文件,如果文件不存在则创建
"""
# 确保目录存在(更健壮)
dir_name = os.path.dirname(path)
if dir_name:
os.makedirs(dir_name, exist_ok=True)
try:
# 使用 utf-8 + ensure_ascii=False 支持中文
with open(path, "w", encoding="utf-8") as f:
json.dump(
data,
f,
ensure_ascii=False,
indent=4
)
except (TypeError, OSError) as e:
raise RuntimeError(f"写入 JSON 失败: {e}")
class LoginView(View):
def get(self, request, *args, **kwargs):
return render(request, "login.html", locals())
def post(self, request, *args, **kwargs):
# 获取前端提交的数据
username = request.POST.get("username")
password = request.POST.get("password")
# 登录校验(模拟账号密码)
if username == "peng" and password == "666":
# 文件
# ⚠️ 建议:这里确保 write_data 已导入
write_data({username: True})
# cookie
obj = HttpResponse(f"{username} 登陆成功!")
# 在浏览器中保存 cookie
# key = sign,value = username
"""
set_cookie 作用:
向浏览器响应头写入 cookie,实现状态保存
"""
# ---------------------------------------------------------
# key:cookie 名称
# value:cookie 值
# max_age:存活时间(秒)
# expires:过期时间(datetime 或时间点)
# path:cookie 生效路径
# domain:cookie 生效域名
# secure:是否只允许 HTTPS
# httponly:是否禁止 JS 访问(防 XSS)
# samesite:防 CSRF(Lax / Strict / None)
# ---------------------------------------------------------
# 标准 datetime 写法 过期时间:当前时间 + 1分钟
# expire_time = datetime.now() + datetime.timedelta(minutes=1)
# obj.set_cookie(
# "sign",
# username,
# expires=expire_time
# )
# Django支持的秒数方式 一分钟过期
obj.set_cookie("sign", username, expires=60)
# session
# Django 会:
# - 对数据进行签名/加密
# - 写入 django_session 表
# - 返回 sessionid 给浏览器
request.session["sign"] = username + password
# 设置 session 过期时间
# 0:关闭浏览器即失效
request.session.set_expiry(0)
return obj
else:
return HttpResponse(f"{username} 登陆失败!")
def read_data():
path = "user_data.json"
if not os.path.exists(path):
with open(path, "w", encoding="utf-8") as f:
json.dump({}, f)
with open(path, "r", encoding="utf-8") as f:
try:
return json.load(f)
except json.JSONDecodeError:
return {}
def index(request):
data = read_data()
if data.get("peng"):
return HttpResponse("index")
else:
return HttpResponse("未登录请先登陆")
def index_one(request):
print(request.COOKIES)
sign = request.COOKIES.get("sign")
if sign == "peng":
return HttpResponse("index_one")
else:
return HttpResponse("未登录请先登陆")
def index_two(request):
# 从请求中获取当前的 session
# 从前端获取到当前传入 的 sessionid ---> 去数据库中取 加密串的数据 ---> 反解 ---> 解除加密的时候放进去的值
sign = request.session.get("sign")
print(sign) # peng666
if sign == "peng666":
return HttpResponse("index_two")
else:
return HttpResponse("未登录请先登陆")
def logout(request):
"""
退出登录逻辑:
1. 删除 Cookie(客户端登录标识)
2. 清除 Session(服务端登录状态)
"""
# 【1】创建响应对象
# HttpResponse:用于返回给浏览器的响应
# 注意:Cookie 的操作必须基于 response 对象
obj = HttpResponse("退出成功!")
# 【2】删除 Cookie(客户端)
# delete_cookie:
# 作用:让浏览器删除指定 key 的 Cookie
#
# sign:
# 一般用于存储登录标识(用户名 / token)
#
# 本质:
# 在响应头中设置:
# Set-Cookie: sign=; Max-Age=0
obj.delete_cookie("sign")
# 【3】删除 Session(服务端)
# ❗ 注意点1:
# request.session 本质是一个 dict-like 对象
#
# ❗ 注意点2:
# Django session 常用操作:
# ------------------------------------------------------
# ❌ delete()(错误/不推荐用法)
# ------------------------------------------------------
request.session.delete()
# ⚠️ Django Session 没有标准 delete 方法(容易报错)
# ------------------------------------------------------
# ✔ flush()(推荐)
# ------------------------------------------------------
# flush 做了三件事:
# 1️⃣ 删除当前 session 数据
# 2️⃣ 删除 session_id cookie
# 3️⃣ 重新生成一个新的 session_key
#
# 👉 本质:彻底清除登录状态
request.session.flush()
# 【4】返回响应
# 返回给浏览器
return obj
# ==============================
# Django CSRF 机制说明
# ==============================
# CSRF(Cross Site Request Forgery)
# Django 默认开启 CSRF 防护(中间件 django.middleware.csrf.CsrfViewMiddleware)
# 作用:防止第三方站点伪造 POST 请求
# ==============================
# FBV(函数视图)方式
# ==============================
# @csrf_protect:开启 CSRF 校验(默认其实就是开启状态)
# 作用:要求 POST 请求必须携带 csrf_token,否则 403
@csrf_protect # 添加保护(显式声明)
# @csrf_exempt # 关闭 CSRF 校验(慎用:等于绕过安全机制)
def login_one(request):
# request.POST:只在 POST 请求时有值
# 如果 CSRF 校验失败,这一行根本不会执行(会直接 403)
print(request.POST)
# render:返回 HTML 页面(三板斧之一 HttpResponse)
return render(request, "login.html", locals())
模拟登录成功后可以正常访问
http://127.0.0.1:8000/index_one/
http://127.0.0.1:8000/index_two/

如果未登录就不能访问

14.Django中CSRF装饰器使用方法
14.1CSRF装饰器简介
CSRF(Cross Site Request Forgery)跨站请求伪造
Django 默认开启 CSRF 防护:
- 针对 POST / PUT / DELETE 请求
- 防止恶意站点伪造用户请求
如果关闭中间件:
- 在
MIDDLEWARE中注释掉'django.middleware.csrf.CsrfViewMiddleware',则 POST 请求会直接报错。
POST请求会报错Forbidden (403)

Django 提供两个核心装饰器:
from django.views.decorators.csrf import csrf_protect, csrf_exemptfrom django.utils.decorators import method_decorator
Django 的 CSRF 机制只校验这些“不安全方法”:
POSTPUTPATCHDELETE
而以下被认为是“安全方法”,不会触发 CSRF 校验:
GETHEADOPTIONSTRACE
开启或关闭用法都是一样的,我这里不分开写了,开启是@csrf_protect,关闭是@csrf_exempt
14.2FBV写法
直接在方法上使用@csrf_exempt 或者@csrf_protect
get请求不会触发 CSRF 校验,如果不加@csrf_exempt ,POST请求会报错Forbidden (403)
# ==============================
# FBV(函数视图)方式
# ==============================
# @csrf_protect:开启 CSRF 校验(默认其实就是开启状态)
# 作用:要求 POST 请求必须携带 csrf_token,否则 403
@csrf_protect # 添加保护(显式声明)
# @csrf_exempt # 关闭 CSRF 校验(慎用:等于绕过安全机制)
def login_one(request):
# request.POST:只在 POST 请求时有值
# 如果 CSRF 校验失败,这一行根本不会执行(会直接 403)
print(request.POST)
# render:返回 HTML 页面(三板斧之一 HttpResponse)
return render(request, "login.html", locals())
@csrf_exempt
# @csrf_protect # 添加保护(显式声明)
def login_one(request):
if request.method == "GET":
# 返回登录页面
return render(request, "login.html")
elif request.method == "POST":
# 只有通过 CSRF 校验,这里才会执行
username = request.POST.get("username")
password = request.POST.get("password")
print(username, password)
return HttpResponse("登录成功")

14.3CBV写法
Django 中类视图不能直接给方法加装饰器(必须借助 method_decorator)
三种方式
# 1️⃣ 直接写在方法上(推荐)
"""
@method_decorator(csrf_protect)
def post(...)
"""
# 2️⃣ 写在类上 + name 指定方法
"""
@method_decorator(csrf_protect, name="post")
class LoginTwoView(View):
"""
# 3️⃣ 写在 dispatch 上(影响所有 HTTP 方法)
"""
@method_decorator(csrf_protect)
def dispatch(...)
"""
示例
# csrf_exempt 同理,但注意:exempt 优先级很高,会关闭验证
@method_decorator(csrf_protect, name="post") # 写在类上 + name 指定方法
class LoginTwoView(View):
# ==============================
# dispatch 方法(核心入口)
# ==============================
# 所有请求(GET/POST/PUT/DELETE)都会先进入 dispatch
#
# Django CBV 请求流程:
# request → url → dispatch → get/post/put...
#
# 所以在 dispatch 上加装饰器 = 作用于所有请求方法
@method_decorator(decorator=csrf_protect)
def dispatch(self, request, *args, **kwargs):
# ==============================
# Django 原生 dispatch 逻辑(简化版)
# ==============================
# 将请求方法转成小写:GET → get
method = request.method.lower()
# 判断是否允许该请求方法(http_method_names 默认:['get','post',...])
if method in self.http_method_names:
# 从当前类中获取对应方法(get/post)
# 如果没有定义 get(),就会用 http_method_not_allowed
handler = getattr(
self,
method,
self.http_method_not_allowed
)
else:
# 不支持的 HTTP 方法
handler = self.http_method_not_allowed
# 执行真正的业务方法(get/post)
return handler(request, *args, **kwargs)
# ==============================
# GET 请求处理
# ==============================
def get(self, request, *args, **kwargs):
# ⚠️ 故意制造异常:int("a") 会抛 ValueError
# 用于测试 Django 错误处理流程 / 调试
int("a")
return render(request, "login.html", locals())
# ==============================
# POST 请求处理
# ==============================
# ❌ 单独写 @csrf_exempt 在这里可能不生效的原因:
# 因为 dispatch 层可能已经处理/拦截 CSRF
#
# ✅ 推荐方式:
# - 要么写在 dispatch
# - 要么写在类上
# - 要么 method_decorator + name
@method_decorator(decorator=csrf_protect)
def post(self, request, *args, **kwargs):
# 打印 POST 数据(表单提交内容)
print(request.POST)
# 返回页面
return render(request, "login.html", locals())
15.Django中间件
15.1简介
Django 的中间件(Middleware)本质上是一个“请求 / 响应处理管道”,它介于 Web服务器 → View → Response 之间,用于对请求和响应做统一处理。
可以理解为:Django 的“全局拦截器 / 过滤器链 / 洋葱模型”。
Django 中间件是一个轻量级的插件系统,用于在:
- 请求进入 View 之前(request阶段)
- View 执行之后(response阶段)
中间件生命周期
特点:先进后出(洋葱模型)
浏览器请求
↓
Middleware 1 (request)
↓
Middleware 2 (request)
↓
Middleware N (request)
↓
View 视图函数
↓
Middleware N (response)
↓
Middleware 2 (response)
↓
Middleware 1 (response)
↓
返回浏览器
15.2内置中间件
SecurityMiddleware安全中间件)
- 作用:提供基础安全增强
- 强制 HTTPS(可配置)
- 设置安全相关 HTTP 头(如 HSTS)
- 防止部分安全攻击(如内容类型嗅探)
- 简单理解:给网站加安全防护层
SessionMiddleware(会话中间件)
- 作用:支持 session 机制
- 解析 Cookie 中的 sessionid
- 读取/写入
request.session - 在响应时更新 session 到数据库/缓存
- 简单理解:让 Django 能记住用户状态
CommonMiddleware(通用中间件)
- 作用:提供一些通用优化和规范处理
- 常见功能:
- URL 规范化(例如自动补
/) - Content-Length 处理
- ETag 支持(缓存优化)
- 处理一些基础请求校验
- URL 规范化(例如自动补
- 请求基础整理 + 规范化
CsrfViewMiddleware(CSRF 防护)
- 作用:防止跨站请求伪造攻击
- 对 POST/PUT/DELETE 等请求校验 CSRF Token
- 防止恶意网站“借用户身份发请求”
- 与
{% csrf_token %}配合使用 - 简单理解:防止别人冒充你提交表单
MIDDLEWARE = [
# ==============================
# 安全相关中间件(Security Layer)
# ==============================
'django.middleware.security.SecurityMiddleware',
# 作用:
# - 强制 HTTPS(可配置 SECURE_SSL_REDIRECT)
# - 设置安全响应头(HSTS 等)
# - 提升整体 HTTP 安全性
# ==============================
# Session 会话管理(Session Layer)
# ==============================
'django.contrib.sessions.middleware.SessionMiddleware',
# 作用:
# - 解析 request.session
# - 从 cookie 获取 sessionid
# - 将 session 数据绑定到 request
# - 请求结束后自动持久化 session
# ==============================
# 通用请求处理(Common Layer)
# ==============================
'django.middleware.common.CommonMiddleware',
# 作用:
# - URL 规范化(是否自动加 /)
# - ETag 缓存支持
# - Host 头检查(防止非法请求)
# ==============================
# CSRF 安全防护(Security Gate)
# ==============================
'django.middleware.csrf.CsrfViewMiddleware',
# 作用:
# - 防止跨站请求伪造(CSRF Attack)
# - 校验 POST/PUT/PATCH/DELETE 请求
# - 验证 csrf_token(cookie vs form)
# - GET/HEAD/OPTIONS 不校验
# ==============================
# 用户认证系统(Auth Layer)
# ==============================
'django.contrib.auth.middleware.AuthenticationMiddleware',
# 作用:
# - 从 session 中获取用户 ID
# - 生成 request.user 对象
# - 提供当前登录用户上下文
# ==============================
# 消息框架(Flash Message Layer)
# ==============================
'django.contrib.messages.middleware.MessageMiddleware',
# 作用:
# - 提供一次性消息机制(success/error/info)
# - 基于 session 存储消息
# - 常用于登录提示、操作反馈
# ==============================
# 点击劫持防护(Clickjacking Protection)
# ==============================
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# 作用:
# - 防止页面被 iframe 嵌套
# - 默认返回 X-Frame-Options: DENY
# - 防止 UI 劫持攻击(钓鱼页面嵌套)
]

15.3中间件核心作用
【1】认证与授权
- request.user 注入
- 登录状态校验
【2】请求预处理
- 修改请求参数
- 添加 header
- 参数合法性校验
【3】响应后处理
- 添加响应头
- 数据包装
- gzip 压缩
【4】异常处理
- 捕获异常
- 记录日志
- 返回统一错误格式
【5】性能优化
- 缓存
- 请求统计
- 限流
15.4旧版Django中间件(Django < 1.10)
旧版中间件通常需要实现多个固定方法,但每个方法是“独立 hook”。
执行特点
-
特点一:分散式钩子
- request / view / response / exception 是分离函数
-
特点二:执行链不统一
-
不像函数调用栈
-
更像“多个拦截器独立生效”
-
-
特点三:必须 return response
- 很容易忘记返回导致报错
class MyMiddleware(object):
def process_request(self, request):
"""请求进入 view 之前"""
pass
def process_view(self, request, view_func, view_args, view_kwargs):
"""URL 匹配之后,view 执行之前"""
pass
def process_response(self, request, response):
"""view 执行之后"""
return response
def process_exception(self, request, exception):
"""view 发生异常时"""
pass
执行流程
request → process_request → process_view → view → process_exception → process_response → response
15.5新版Django中间件(Django ≥ 1.10)
新版中间件是“函数式中间件风格 + 类包装”
执行特点
-
特点一:责任链模式(Chain of Responsibility),中间件变成“嵌套函数调用”:
middleware1(middleware2(middleware3(view))) -
特点二:结构统一
- 只有一个核心入口:
__call__ - 其他 hook 是“补充能力”
- 只有一个核心入口:
-
特点三:天然支持上下文包裹
# 类似: before view() after -
特点四:必须调用 get_response,否则请求链断裂。
class MyMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
print("请求前")
response = self.get_response(request)
print("响应后")
return response
def process_exception(self, request, exception):
"""可选:异常处理"""
pass
def process_view(self, request, view_func, view_args, view_kwargs):
"""可选:view 前处理"""
pass
执行流程
request
↓
Middleware1.__call__
↓
Middleware2.__call__
↓
view
↑
Middleware2 response
↑
Middleware1 response
15.6自定义中间件
旧版写法
class MiddlewareOne(MiddlewareMixin):
# 如何在中间件中构造 一个限制 每一个IP1分钟只能只能访问 5 次的 频率限制
def process_request(self, request):
# print(request.META)
# REMOTE_ADDR = request.META.get("REMOTE_ADDR")
# if REMOTE_ADDR == '127.0.0.1':
# return HttpResponse("本地IP不允许访问")
# print("process_request :>>>> MiddlewareOne")
logger.info("process_request :>>>> MiddlewareOne")
# 在真正的进入视图函数之前进行处理的逻辑
def process_view(self, request, callback, callback_args, callback_kwargs):
# print("process_view :>>>> MiddlewareOne")
logger.info("process_view :>>>> MiddlewareOne")
def process_response(self, request, response):
# print("process_response :>>>> MiddlewareOne")
logger.info("process_response :>>>> MiddlewareOne")
return response
# 如果你的Django视图报错会自动被当前拦截到
def process_exception(self, request, exception):
# print("process_exception :>>>> MiddlewareOne")
# print(f"process_exception :>>>> {exception}")
logger.info("process_exception :>>>> MiddlewareOne")
logger.info(f"process_exception :>>>> {exception}")
return redirect(reverse("login_one"))
新版写法
import time
import uuid
import logging
from django.http import JsonResponse
logger = logging.getLogger("django.request")
class RequestLogMiddleware:
"""
生产级中间件(日志 + 性能 + 异常 + trace_id)
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# ===============================
# 【1】生成 trace_id(链路追踪)
# ===============================
trace_id = str(uuid.uuid4())
request.trace_id = trace_id
# ===============================
# 【2】记录开始时间
# ===============================
start_time = time.time()
# ===============================
# 【3】请求日志
# ===============================
logger.info({
"type": "request",
"trace_id": trace_id,
"method": request.method,
"path": request.path,
"ip": self.get_client_ip(request),
"params": self.get_request_params(request),
})
try:
# ===============================
# 【4】执行后续中间件 / 视图
# ===============================
response = self.get_response(request)
except Exception as e:
# ===============================
# 【5】异常统一处理
# ===============================
logger.exception({
"type": "error",
"trace_id": trace_id,
"error": str(e),
"path": request.path,
})
return JsonResponse({
"code": 500,
"msg": "服务器内部错误",
"trace_id": trace_id
}, status=500)
# ===============================
# 【6】计算耗时
# ===============================
cost_time = round(time.time() - start_time, 3)
# ===============================
# 【7】响应日志
# ===============================
logger.info({
"type": "response",
"trace_id": trace_id,
"status_code": response.status_code,
"cost_time": cost_time,
})
# ===============================
# 【8】把 trace_id 写入响应头(方便排查)
# ===============================
response["X-Trace-Id"] = trace_id
return response
# ======================================
# 工具方法
# ======================================
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]
return request.META.get("REMOTE_ADDR")
def get_request_params(self, request):
"""获取请求参数"""
if request.method == "GET":
return request.GET.dict()
elif request.method == "POST":
return request.POST.dict()
return {}
配置中间件
MIDDLEWARE = [
...
# 自定义中间件
'user.middwares.MiddlewareOne',
'user.middwares.RequestLogMiddleware',
]

先进MiddlewareOne,再进RequestLogMiddleware,先进后出

16.CSRF跨站请求伪造
16.1简介
CSRF(Cross-Site Request Forgery)是一种常见的 Web 攻击方式
核心思想:攻击者诱导用户访问一个“伪造页面”,利用用户已经登录的身份,在不知情的情况下发起请求
本质:借用你的身份去做坏事(浏览器自动携带 cookie)
举例
- 用户访问钓鱼网站 → 自动向真实网站发起转账请求
- 用户误以为是正常操作,实际上已经被攻击
CSRF 攻击原理
- 用户已登录目标网站(浏览器有 cookie)
- 用户访问攻击者网站
- 攻击者构造请求(POST / GET)
- 浏览器会自动携带 cookie,服务端误认为是“用户本人操作”
Django 防御 CSRF 的核心机制
- 服务端生成 csrf_token
- 返回给前端(隐藏字段 or cookie)
- 前端提交请求时必须携带
- 服务端校验 token
CSRF 防御方案总结
- 1.
CSRF Token(核心方案): 每个表单必须携带 csrf_token - 2.
SameSite Cookie: 限制跨站请求携带 cookie - 3.
Referer 校验: 判断请求来源是否合法 - 4.验证码(增强安全): 防止自动化攻击
16.2解决csrf 403问题
方案一:直将中间件注释掉django.middleware.csrf.CsrfViewMiddleware,只用于测试,不推荐生产

方案二:类装饰器或者方法装饰器
# 1️⃣ 直接写在方法上(推荐)
"""
@method_decorator(csrf_exempt)
def post(...)
"""
# 2️⃣ 写在类上 + name 指定方法
"""
@method_decorator(csrf_exempt, name="post")
class LoginTwoView(View):
"""
# 3️⃣ 写在 dispatch 上(影响所有 HTTP 方法)
"""
@method_decorator(csrf_exempt)
def dispatch(...)
"""

方案三:在 form 表单内增加一个 csrf_token 的标签,这样就不用关闭csrf_exempt
<form action="" method="post">
{% csrf_token %}
<p>
请输入目标用户账号 : <input type="text" name="to_account" value="peng">
</p>
<p>
请输入付款金额 : <input type="text" name="pay_money" value="666">
</p>
<p>
<input type="submit">
</p>
</form>
表单会自动生成一个随机的token
<input type="hidden" name="csrfmiddlewaretoken" value="dovilDXJ9IIiIdSnQmncOUe8ZowlwRPMZ3JzV4bnyx6E4d13W0Fsj0ltTshYy1tI">

16.3模拟钓鱼网站
用户已经登录真实网站(localhost:8000)并持有有效的 Cookie,随后访问钓鱼网站时,钓鱼页面构造了一个指向 localhost:8000/pay/ 的 POST 表单,并通过隐藏字段篡改 to_account 为“钓鱼网站”,诱导用户点击提交;此时浏览器会自动携带用户在真实网站的 Cookie(session)一起发送请求,服务器仅依据 Cookie 识别用户身份,在你关闭 CSRF 校验(csrf_exempt)的情况下,无法判断该请求是否来自合法页面,最终将恶意参数当作用户真实意图执行,从而完成一次伪造的转账操作——本质就是利用“浏览器自动带凭证 + 服务端不校验请求来源”实现的跨站请求伪造攻击。
服务8000
# 【1】真正的网站
"""
<form action="" method="post">
<p>
请输入目标用户账号 : <input type="text" name="to_account" value="peng">
</p>
<p>
请输入付款金额 : <input type="text" name="pay_money" value="666">
</p>
<p>
<input type="submit">
</p>
</form>
"""
"""
@method_decorator(csrf_exempt, name='dispatch') # 关闭 CSRF(演示用,生产不推荐)
# @method_decorator(csrf_protect, name='dispatch') # 开启 CSRF
class CSRFView(View):
def get(self, request, *args, **kwargs):
return render(request, "csrf.html", locals())
def post(self, request, *args, **kwargs):
print(request.POST)
to_account = request.POST.get("to_account")
# pay_password = request.POST.get("pay_password")
pay_money = request.POST.get("pay_money")
return HttpResponse(f"向目标账户 {to_account} 付款 {pay_money}")
"""
服务8001
# 【2】钓鱼的网站
"""
<form action="http://localhost:8000/pay/" method="post">
<p>
请输入目标用户账号 : <input type="text" value="peng">
<input type="text" name="to_account" value="钓鱼网站" hidden>
</p>
<p>
请输入付款金额 : <input type="text" name="pay_money" value="888">
</p>
<p>
<input type="submit">
</p>
</form>
"""

16.4Ajax提交POST请求
方案一:直接根据标签的 name 属性获取标签所对应的值,上面添加{% csrf_token %}会自动生成一个隐藏域,ajax发送请求是获取这个隐藏域标签的值
<input type="hidden" name="csrfmiddlewaretoken" value="oH4ZLx1jnbNeExDlK1IvhvbTsHPqIjE1amiglYfXM0bA0xM1QF0LMBiemLA3KtiX">
let csrfmiddlewaretoken = $("input[name='csrfmiddlewaretoken']").val()
{#console.log(csrfmiddlewaretoken)#}
方案二:利用Django提供给我们的模版语法,{{csrf_token}}获取csrf_token的值
{% csrf_token %}
{{csrf_token}}
方案三:Ajax官网提供给我们的方案 创建一个 js 脚本并引入 会自动将 csrftoken 增加到请求头参数中
<script src="{% static 'js/csrf_except.js' %}"></script>

我这里只演示导入csrf_except.js的方法
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="{% static 'js/jquery.min.js' %}"></script>
</head>
<body>
<form>
{% csrf_token %}
<p>
请输入目标用户账号 : <input type="text" name="to_account" id="to_account" value="peng">
</p>
<p>
请输入付款金额 : <input type="text" name="pay_money" id="pay_money" value="666">
</p>
<p>
<input type="button" value="提交" id="btn_submit">
</p>
</form>
<script src="{% static 'js/csrf_except.js' %}"></script>
<script>
$("#btn_submit").click(function () {
let account = $("#to_account").val()
{#let account = $("input[name='to_account']").val()#}
let money = $("#pay_money").val()
{#let money =$("input[name='pay_money']").val()#}
{#<input type="hidden" name="csrfmiddlewaretoken" value="tS83ry1qy3tmhvrtZhlUhyjIDKsvdprvCvkEgyugTewkU2kMcXEo8pH6XSrCxiwk">#}
let csrfmiddlewaretoken = $("input[name='csrfmiddlewaretoken']").val()
{#console.log(csrfmiddlewaretoken)#}
$.ajax({
url: "",
type: "post",
data: {
"to_account": account,
"pay_money": money,
{#"csrfmiddlewaretoken": csrfmiddlewaretoken,#}
{#"csrfmiddlewaretoken": "{{csrf_token}}"#}
},
success: function (data) {
console.log(data)
}
})
})
</script>
</body>
</html>

16.5总结
CSRF 是利用浏览器自动携带 Cookie 的机制,伪造用户请求的一种攻击方式。
Django 防御核心
- CSRF Token
- Cookie + Token 双重校验
- CsrfViewMiddleware
开发建议
- 表单必须加
{% csrf_token %} - Ajax 必须带 token
- 不要随意使用
csrf_exempt
17.Django的auth模块
17.1简介
Django 自带的认证与权限管理系统(Authentication & Authorization)
主要用于:
-
1.用户登录认证
-
2.权限控制
-
3.用户分组管理
-
4.RBAC 权限模型实现
Django 自带 auth 表
| 表名 | 作用 | 核心字段 | 说明 |
|---|---|---|---|
auth_user |
用户表 | id, username, password, email, is_superuser, is_staff, is_active | Django 默认用户模型 |
auth_group |
角色/用户组 | id, name | 用于角色管理(RBAC中的Role) |
auth_permission |
权限表 | id, name, codename, content_type_id | 定义“能做什么操作” |
auth_user_groups |
用户-组关系表 | id, user_id, group_id | 多对多:用户属于哪些组 |
auth_group_permissions |
组-权限关系表 | id, group_id, permission_id | 组拥有哪些权限 |
auth_user_user_permissions |
用户-权限关系表 | id, user_id, permission_id | 用户直接绑定权限(绕过组) |
Django 内容类型表(权限绑定核心)
| 表名 | 作用 | 核心字段 | 说明 |
|---|---|---|---|
django_content_type |
模型注册表 | id, app_label, model | 标识“哪个app的哪个模型” |
django_migrations |
迁移记录表 | id, app, name, applied | 记录数据库迁移历史 |
会话与缓存相关(auth 登录依赖)
| 表名 | 作用 | 核心字段 | 说明 |
|---|---|---|---|
django_session |
会话表 | session_key, session_data, expire_date | 登录态保存(session机制) |
自定义用户模型(项目中常见)
| 表名 | 作用 | 说明 |
|---|---|---|
user_user |
自定义用户表 | 继承 AbstractUser 扩展字段 |
user_user_groups |
自定义用户-组关系 | 替代 auth_user_groups |
user_user_user_permissions |
自定义用户-权限关系 | 替代 auth_user_user_permissions |

核心思想(RBAC)
RBAC = Role Based Access Control(基于角色的权限控制)
核心模型:
用户(User)
↓ 属于
角色(Group)
↓ 拥有
权限(Permission)
支持:
- 用户级权限
- 角色级权限
- 灵活扩展权限控制
Django 判断权限流程
- 1.用户登录(auth_user)
- 2.查询用户属于哪个组(auth_user_groups)
- 3.获取组对应权限(auth_group_permissions)
- 4.判断是否允许访问某个功能(auth_permission)
17.2超级管理员
Django 自带的后台管理系统(admin site)
默认访问路径: /admin/
功能
- 管理数据库数据(CRUD)
- 管理用户 / 权限 / 业务数据
- 快速开发后台系统
创建用户需要完成所有数据库的迁移
迁移命令
# 扫描所有 app 生成迁移文件
makemigrations
# 只生成 user 迁移文件
makemigrations user
# 只生成 admin 迁移文件
makemigrations admin
# 执行所有迁移
migrate
创建命令,注意
- createuser(Django 没有这个命令,容易误记,我试了也没有这个命令)
- 只有 createsuperuser
python manage.py createsuperuser
首先要完成所有模块的迁移,我这里不知道为什么不能自动迁移所有模块,需要手动迁移自定义模块user和admin模块
然后输入用户名和密码,我这里设置的是admin/admin,中间跳过密码验证即可

然后访问后台登录
http://127.0.0.1:8000/admin

auth_user表结构总览
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | AutoField | 主键 |
| password | varchar | 加密后的密码 |
| last_login | datetime | 最后登录时间 |
| is_superuser | bool | 是否超级用户 |
| username | varchar | 用户名(唯一) |
| first_name | varchar | 名 |
| last_name | varchar | 姓 |
| varchar | 邮箱 | |
| is_staff | bool | 是否可进 admin 后台 |
| is_active | bool | 是否激活账号 |
| date_joined | datetime | 注册时间 |
auth_user表

17.3注册用户
User 模型来源
Django 内置用户模型:from django.contrib.auth.models import User
数据存储表:auth_user
用户名唯一性约束
- auth_user.username 是唯一字段
- 报错:
django.db.utils.IntegrityError:UNIQUE constraint failed: auth_user.username - 说明:用户名不能重复
直接 create() 的问题
- 错误方式:
User.objects.create(username=username, password=password) - 密码是明文存储(不加密)
- Django 登录系统只识别加密密码
- 导致无法登录(authenticate 失败)
正确创建用户方式
- 正确方式:
User.objects.create_user(username=username, password=password) - 自动加密密码(pbkdf2_sha256)
- 可用于登录系统
- 自动调用set_password()`
创建超级管理员
User.objects.create_superuser(username=username, password=password)- is_superuser = True: 超级用户
- is_staff = True: 可登录后台
@method_decorator(csrf_protect, name='dispatch') # 开启 CSRF
class RegisterView(View):
def get(self, request, *args, **kwargs):
"""
处理 GET 请求:
返回注册页面
"""
return render(request, "register.html", locals())
def post(self, request, *args, **kwargs):
"""
处理 POST 请求:用户注册逻辑
"""
username = request.POST.get("username")
password = request.POST.get("password")
if User.objects.filter(username=username).exists():
return HttpResponse("用户名已存在")
# User.objects.create
# 1.密码是明文存储(不加密)
# 2.Django 登录系统只识别加密密码
# 3.导致无法登录(authenticate 失败)
# User.objects.create(username=username, password=password)
# 创建超级管理员
# 1.is_superuser = True
# 2.is_staff = True
# 3.可登录 admin 后台
# User.objects.create_superuser(username=username, password=password)
# 创建普通用户
# 1.自动加密密码(pbkdf2_sha256)
# 2.可用于登录系统
# 3.自动调用 set_password()
User.objects.create_user(username=username, password=password)
return HttpResponse(f"{username} 创建成功!")

17.4auth模块方法
Django Auth = 内置用户认证系统
基于默认模型表:
- auth_user -> 用户表
- auth_group -> 角色表
- auth_permission -> 权限表
使用前必须:
python manage.py makemigrations
python manage.py migrate
创建普通用户
User.objects.create_user(
username=username,
password=password
)
创建超级管理员
User.objects.create_superuser(
username=username,
password=password
)
登录认证
user_obj = auth.authenticate(
request,
username=username,
password=password
)
保存登录状态
auth.login(request, user_obj)
退出登录
auth.logout(request)
当前登录用户
print(request.user)
判断是否登录
# 推荐方式(标准)
if request.user.is_authenticated:
print("已登录")
else:
print("未登录")
---------------------------------------------------------
# 不推荐方式
if request.user == "AnonymousUser":
pass
登录装饰器
=========================================================
✔ 方式一:局部配置跳转地址
=========================================================
@login_required(login_url="/login/")
def index(request):
return HttpResponse("已登录才能访问")
=========================================================
✔ 方式二:全局配置(推荐)
=========================================================
settings.py:
LOGIN_URL = "/login/"
使用:
@login_required
def index(request):
return HttpResponse("已登录才能访问")
修改密码
【1】验证旧密码
request.user.check_password(old_password)
返回:
True -> 正确
False -> 错误
---------------------------------------------------------
【2】修改密码(必须用 auth 方法)
request.user.set_password(new_password)
request.user.save()
注意:
set_password 才会自动加密密码
总结
使用 auth 模块必须“全套使用”
正确组合:
- create_user / create_superuser
- authenticate
- login
- logout
- set_password
- check_password
不要混用
- ORM create()
- 手动写 password
- 手动查 password
示例
@method_decorator(csrf_protect, name='dispatch') # 开启 CSRF
class LoginView(View):
def get(self, request, *args, **kwargs):
"""
处理 GET 请求:
返回注册页面
"""
return render(request, "register.html", locals())
def post(self, request, *args, **kwargs):
# =========================
# 【一】获取到用户名和密码
# =========================
username = request.POST.get("username", "").strip()
password = request.POST.get("password", "").strip()
# =========================
# 【二】基础参数校验(优化新增)
# =========================
if not username or not password:
return HttpResponse("用户名或密码不能为空!")
# =========================
# 【三】比对用户名和密码
# =========================
# 【1】直接模型表 filter 发现查不出来 --- 原因是密码是密文而我们传进来的事明文
# obj = User.objects.filter(username=username,password=password)
# print(obj) # <QuerySet []>
# 【2】借助 auth 模块完成登陆认证
# authenticate(request,username=传入的,password=传入的)
# (1)如果用户名和密码正确就返回正确的用户对应的对象
user_obj = auth.authenticate(
request,
username=username,
password=password
)
# (2)如果用户名和密码错误就会返回None
print(user_obj, type(user_obj))
# jack <class 'django.contrib.auth.models.User'>
# =========================
# 【4】登陆功能实现
# =========================
if not user_obj:
return HttpResponse(f"当前用户 {username} 用户名或密码错误!")
# =========================
# 【5】保存登录状态(session)
# =========================
# print(request.user.is_authenticated) # False
auth.login(request, user_obj)
# print(request.user.is_authenticated) # True
# =========================
# 【6】登录后跳转逻辑
# =========================
# print(request.get_full_path()) # /login/?next=/index/
next_url = request.GET.get("next")
# print(next_url) # /index/
# ⚠️ 优化:防止 open redirect(可选增强)
# 这里只允许站内跳转
if next_url and next_url.startswith("/"):
return redirect(next_url)
return HttpResponse(f"当前用户 {user_obj.username} 登陆成功!")
def home(request):
# ==============================
# 【1】方式一 : 通过 request.user 查看当前登录的用户对象
# ==============================
print(request.user) # AnonymousUser
# 如果没有登录过 没有保存用户状态 就是 AnonymousUser 匿名用户
# 如果登陆成功后 打印的就是当前已经登陆的 用户对象 dream_1
print(type(request.user)) # <class 'django.utils.functional.SimpleLazyObject'>
# if request.user == "AnonymousUser":
# return HttpResponse(f"未登录请先登陆!")
# ==============================
# 【2】方式二 : request.user.is_authenticated 获取当前用户的登陆状态
# ==============================
if request.user.is_authenticated:
# ==============================
# 已登录用户访问
# ==============================
return HttpResponse("登录后才能看到的页面")
else:
# ==============================
# 未登录用户访问(修复逻辑)
# ==============================
return HttpResponse("未登录,请先登录!")
def login_auth(func):
def inner(request, *args, **kwargs):
if request.user.is_authenticated:
result = func(request, *args, **kwargs)
return result
else:
# 重定向到登陆页面
# http://localhost:8000/admin/login/?next=/admin/
now_path = request.path_info
print(now_path) # /index/
print(reverse("login")) # /login/
new_url = reverse("login") + '?' + f"next={now_path}"
print(new_url) # /login/?next=/index/
return redirect(new_url)
return inner
@login_auth
def index(request):
int("a")
# 做一个登陆认证重定向
# 只有登录后什么都不干 没有登陆的时候必须重定向到登陆页面进行登录
return HttpResponse("登录后才能看到的页面")
from django.contrib.auth.decorators import login_required
# auth 模块提供给我们一个登录跳转的配置 --- 局部配置
# 局部配置跳转的登陆地址
# @login_required # 不指定跳转地址 显示的是
# http://localhost:8000/accounts/login/?next=/func/
# http://127.0.0.1:8000/accounts/login/?next=/func/
# 【方案一】:自己指定跳转的登陆地址
# @login_required(login_url="/login/")
# 【方案二】但是要在全局的配置文件中指定跳转的地址 LOGIN_URL = '/login/'
# @login_required
@login_required(login_url="/login/")
def func(request):
# 做一个登陆认证重定向
# 只有登录后什么都不干 没有登陆的时候必须重定向到登陆页面进行登录
return HttpResponse("登录后才能看到的页面")
def logout(request):
# auth 模块注销
auth.logout(request)
return redirect(reverse("login"))
# ==============================
# 必须登录才能修改密码
# ==============================
@method_decorator(login_required, name='dispatch')
class ChangePwdView(View):
def get(self, request, *args, **kwargs):
"""
处理 GET 请求:
返回修改密码页面
"""
# request.user 已经是登录用户(login_required保证)
# 所以这里不需要再判断登录状态
return render(request, "change_password.html", locals())
def post(self, request, *args, **kwargs):
# ==============================
# 【1】获取前端提交的数据
# ==============================
username = request.POST.get("username")
old_password = request.POST.get("old_password")
new_password = request.POST.get("new_password")
# ==============================
# 【2】安全校验:验证旧密码是否正确
# ==============================
# check_password() 会自动用 Django 的密码哈希进行比对
# 不能直接 request.user.password 比较(是密文)
if not request.user.check_password(old_password):
return HttpResponse("密码错误!")
# ==============================
# 【3】设置新密码(关键)
# ==============================
# set_password() 会自动:
# ✔ 对新密码进行加密
# ✔ 替换旧密码 hash
request.user.set_password(new_password)
# ==============================
# 【4】保存用户对象
# ==============================
request.user.save()
# ==============================
# 【5】重要:退出当前登录状态
# ==============================
# 因为修改密码后 session 失效(安全机制)
logout(request)
# ==============================
# 【6】返回结果
# ==============================
return HttpResponse(f"{username} 修改密码成功!")

17.5User扩展
Django 默认用户表:auth_user
默认问题:
- 字段不可扩展(不能随便加 phone、gender)
- 后期项目升级困难
- 企业项目基本不用默认 User
推荐方式:继承 AbstractUser
from django.contrib.auth.models import AbstractUser
from django.db import models
# Create your models here.
class User(AbstractUser):
phone = models.CharField(null=True, blank=True,max_length=11, verbose_name="手机号")
gender = models.IntegerField(null=True, blank=True,verbose_name="性别")

settings.py配置AUTH_USER_MODEL
# 增加一个配置
# 扩展 User表所在的 app.扩展的表名
AUTH_USER_MODEL = "user.User"

数据库迁移(如果不成功,建议把之前的数据库和迁移文件删了,正常来说一开始也不会直接使用user表)
- user_user = 现在用的
- auth_user = 历史遗留(可以不用管)
# 迁移命令
makemigrations
makemigrations admin
makemigrations user
migrate
# 以下是完整迁移过程
manage.py@demo141 > makemigrations
Tracking file by folder pattern: migrations
Migrations for 'admin':
+ Create model LogEntry
进程已结束,退出代码为 0
manage.py@demo141 > makemigrations admin
Tracking file by folder pattern: migrations
No changes detected in app 'admin'
进程已结束,退出代码为 0
manage.py@demo141 > makemigrations user
Tracking file by folder pattern: migrations
Migrations for 'user':
user\migrations\0001_initial.py
+ Create model User
Following files were affected
manage.py@demo141 > migrate
Tracking file by folder pattern: migrations
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, user
Running migrations:
Applying contenttypes.0001_initial... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0001_initial... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying user.0001_initial... OK
Applying admin.0001_initial... OK
Applying sessions.0001_initial... OK
进程已结束,退出代码为 0
统一使用 get_user_model
# 这是没有扩展 AbstractUser 使用的 User
# from django.contrib.auth.models import AbstractUser, User
# 这是扩展 AbstractUser 使用的 User
from django.contrib.auth import get_user_model
@method_decorator(csrf_protect, name='dispatch') # 开启 CSRF
class RegisterView(View):
def get(self, request, *args, **kwargs):
"""
处理 GET 请求:
返回注册页面
"""
return render(request, "register.html", locals())
def post(self, request, *args, **kwargs):
"""
处理 POST 请求:用户注册逻辑
"""
# 这是扩展 AbstractUser 使用的 User
User = get_user_model()
username = request.POST.get("username")
password = request.POST.get("password")
if User.objects.filter(username=username).exists():
return HttpResponse("用户名已存在")
# User.objects.create
# 1.密码是明文存储(不加密)
# 2.Django 登录系统只识别加密密码
# 3.导致无法登录(authenticate 失败)
# User.objects.create(username=username, password=password)
# 创建超级管理员
# 1.is_superuser = True
# 2.is_staff = True
# 3.可登录 admin 后台
# User.objects.create_superuser(username=username, password=password)
# 创建普通用户
# 1.自动加密密码(pbkdf2_sha256)
# 2.可用于登录系统
# 3.自动调用 set_password()
User.objects.create_user(username=username, password=password)
return HttpResponse(f"{username} 创建成功!")

18.Django的配置(settings)
Django 配置 = 两层结构
- global_settings(Django 默认配置)
- 项目 settings.py(用户自定义配置)
方式一: 直接导入项目配置文件(不推荐)
- 只能拿到你自己写的配置
- 拿不到 Django 默认配置(global_settings)
- 可扩展性差
from demo14 import settings
方式二: 直接导入项目配置文件(不推荐)
- 本质:LazySettings(懒加载对象)
- 统一整合:global_settings + 项目 settings.py
from django.conf import settings
19.Django动态导入模块(importlib)
方式1:动态导入(字符串)
优点:
- 支持字符串(动态路径)
- Django 框架内部用的就是这个
- 可用于配置切换(dev / prod)
- 本质:运行时导入模块
mod = importlib.import_module(SETTINGS_MODULE)
方式2:静态导入
缺点:
- 写死路径
- 不够灵活
from demo14 import settings
📌 创作不易,感谢支持!
每一篇内容都凝聚了心血与热情,如果我的内容对您有帮助,欢迎请我喝杯咖啡☕,您的支持是我持续分享的最大动力!
💬 加入交流群(QQ群):576434538

浙公网安备 33010602011771号