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

image-20260315020616024

查看当前所有环境

conda info --envs

image-20260315020933545

下面的安装过程放在一起了

# 激活环境
conda activate django_env

# 安装 Django 5.2 LTS
pip install Django==5.2

# 验证 Django 是否安装成功
django-admin --version

# 查看当前环境已安装的包
pip list      # pip 安装的包
conda list    # conda 安装的包

image-20260315021421341

1.4Django命令行管理工具

安装之后会有django-admin.exe

image-20260315021840206

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

image-20260315022119969

1.6创建项目

1.6.1命令创建

django-admin startproject 项目名

django-admin startproject demo01

image-20260315022538102

1.6.2Pycharm创建

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

image-20260315023233685

1.7启动项目

1.7.1命令行启动

在和 manage.py同目录下执行

# 指定端口
# python manage.py runserver 8080
# 默认是 8000
python manage.py runserver

默认启动在 http://127.0.0.1:8000/

image-20260315023905719

1.7.2Pycharm启动

也可以配置端口等参数

image-20260315024206967

然后点击运行

image-20260315024259363

1.7.3访问

运行成功后可以使用浏览器访问

http://127.0.0.1:8000/

image-20260315024342033

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名字

image-20260315211220823

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

image-20260315211321350

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

image-20260315211650062

1.8.3Pycharm创建App

在 工具 中选择 运行manage.py任务

然后输入命令

# 示例
# startapp shop
startapp 项目名称

image-20260315211505286

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

image-20260315211821765

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 # 自己写的业务逻辑的地方

image-20260315221511980

2.Django视图三板斧

在 Django 的 views.py 中,最常用的返回方式有三种:

方法 用途 返回
HttpResponse 返回字符串 文本
render 返回 HTML 页面 模板
redirect 跳转页面 新 URL

2.1render

2.1.1简介

render 是一个 快捷函数,用于 将模板渲染成 HTML 响应 并返回给浏览器。

它是 HttpResponse 的封装,本质上最终还是返回一个 HttpResponse 对象。

参数说明

参数 说明
request 必须,当前请求对象,模板渲染时可以用它访问 request.userrequest.session
template_name 模板路径,如 'index.html',可以相对 app templates 目录
context 字典,模板上下文,用于传递数据给模板
content_type 可选,设置返回的 Content-Type,比如 'text/html; charset=utf-8'
status 可选,设置 HTTP 状态码,比如 404500
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)

image-20260318012546504

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")

image-20260318013904336

2.1.3配置模板系统

在项目的settings.py可配置模版系统

一般DIRS配置空数组和APP_DIRS开启自动扫描,剩下默认即可,Django会自动扫描每个apptemplates文件夹

image-20260318012859006

TEMPLATES的参数说明

配置 / 功能 说明 示例 / 注意点
BACKEND 模板引擎 Django 默认:django.template.backends.django.DjangoTemplates
Jinja2:django.template.backends.jinja2.Jinja2
DIRS 项目级模板目录 [BASE_DIR / 'templates'],空时依赖 APP_DIRS
APP_DIRS 自动扫描 app 模板 True → 扫描每个 app 的 templates 文件夹
context_processors 全局模板变量 requestusermessages
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>

image-20260318014205586

2.1.5注册路由

urlpatterns = [
    path('index/', index)
]

为什么index/要加/?

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

image-20260318014900304

2.1.6测试

启动项目,打开8000端口(默认),页面显示我们配置的所有路由

http://127.0.0.1:8000/

image-20260318020757987

访问

http://127.0.0.1:8000/index/

image-20260318020822299

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,
    )

image-20260318015759070

临时重定向和永久重定向

临时重定向(响应状态码: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")

image-20260318020138164

2.2.3注册路由

urls.py注册路由

urlpatterns = [
    path('admin/', admin.site.urls),
    path('index/', index),
    path('func/', func),
]

image-20260318020320565

2.2.4测试

访问func会自动跳转到https://www.baidu.com/

http://127.0.0.1:8000/func/

image-20260318020926740

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 状态码 200404500
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    
     ...

image-20260318021832146

2.3.2定义视图函数

这里就简单返回字符串

def aaa(request):
    return HttpResponse("aaa")

image-20260318022109882

2.3.3注册路由

urls.py注册aaa/

urlpatterns = [
    path('admin/', admin.site.urls),
    path('index/', index),
    path('func/', func),
    path('aaa/', aaa)
]

image-20260318022236452

2.3.4测试

访问http://127.0.0.1:8000/aaa/

image-20260318022256828

3.Django静态文件系统

3.1静态文件

静态文件的分类

  • 模板文件
    • templates 文件夹
      • 存放 HTML 页面源码
  • 资源文件
    • static 文件夹
      • css 样式文件
      • js 脚本文件
      • 第三方前端框架 (bootstrap)
      • 图片文件
      • 视频文件

静态文件的目录

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>

image-20260318022810616

3.2.2配置STATIC_URLSTATICFILES_DIRS

地址: https://docs.djangoproject.com/en/5.2/howto/static-files/

image-20260318023330335

配置STATIC_URLSTATICFILES_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"

image-20260318023405825

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' %}">

image-20260318024150487

3.2.4测试

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

image-20260318024350585

4.request对象

4.1简介

这里的 request

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

image-20260318165601915

常用属性

属性名 类型 说明 示例
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视图,可接收GetPost请求

@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")

image-20260318170101126

4.3注册路由

注册login路由

urlpatterns = [
    path('login/', login)
]

image-20260318170220593

4.4定义模版文件

登录页面负责发送Get或者Post请求,

  • method="get": 发送get请求
  • method="post": 发送post请求

image-20260318170538744

4.5测试Get请求

点击submit发送了get请求

  • dir(request): 查看 request 对象所有属性和方法
  • request.method: 获取 HTTP 请求方式
  • request.GET.get(): 获取 GET 请求参数

image-20260318170722346

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 校验

image-20260318233733442

修改login.htmlmethod="post",然后点击点击submit发送了post请求

  • request.POST.get: 获取 POST 请求参数

image-20260318171632963

5.Django连接Mysql

5.1docker安装mysql:latest

我这里使用docker搭建测试环境比较方便

下载镜像

在 Docker 里,:latest一个镜像标签(tag),表示“该镜像库中官方标记的最新稳定版本”。它不是固定版本号,而是一个 动态指向当前最新发布的版本

# 或者 mysql:8.0
docker pull mysql:latest

image-20260316235434299

查看镜像有没有下载成功

docker images

image-20260317002819220

创建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

image-20260317005148147

编辑配置文件

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'

image-20260317005233617

运行镜像:

  • --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                             

image-20260317005822353

可以查看容器是否运行

docker ps

image-20260317005830663

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',                          # 数据库端口
    }
}

image-20260318172509459

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'

image-20260316230252862

第一种方式: 安装mysqlclient,官方推荐

直接安装mysqlclient,官方推荐,我使用的就是这种方式

pip install mysqlclient

image-20260316230450531

第二种方式安装: PyMySQL

安装PyMySQL,我这里记录一下这种方式

pip install mysqlclient

项目的 __init__ 文件进行导入

import pymysql
pymysql.install_as_MySQLdb()

image-20260318173243069

5.4创建数据库

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

image-20260318173510055

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

image-20260318173616373

5.6启动项目

项目正常启动即可

image-20260318173752070

5.5Django后台管理系统

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

image-20260318203148109

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

image-20260318203350387

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

image-20260318201837810

6.3生成迁移记录

python manage.py makemigrations:

  • 作用:生成迁移文件(migration file),记录你在 models.py 中对数据库模型的新增、修改、删除操作。它不会直接修改数据库,只是生成一个 Python 文件,告诉 Django 如何把模型的变化应用到数据库。
  • 典型场景
    • 新建模型(表)
    • 新增字段
    • 修改字段类型
    • 删除字段或模型

python manage.py migrate:

  • 作用:执行迁移,将 makemigrations 生成的迁移文件中的操作真正应用到数据库中,创建或修改表结构。换句话说,它真正改变数据库
  • 典型场景
    • 初始化数据库(第一次迁移)
    • 应用新增字段或模型
    • 回滚到之前的迁移版本(通过 --fakemigrate <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> 

image-20260318202443308

迁移文件在migrations目录下

image-20260318203617185

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

image-20260318204127193

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

image-20260318204359929

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

image-20260318204728184

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")

image-20260318213258801

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)
]

image-20260318222431658

访问

http://127.0.0.1:8000/add111/

但是仍匹配到

http://127.0.0.1:8000/add/

image-20260318223215259

除非路劲后加一个分隔符 / ,可正常匹配路由

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)
]

image-20260318223101284

使用path也可解决

Django 默认配置 APPEND_SLASH = True ,当 URL 没匹配成功时,Django 会尝试在末尾加一个 / 再匹配一次

为了避免路由匹配歧义、保持 URL 规范以及兼容中间件,推荐在 path 路由定义时统一以 / 结尾。

urlpatterns = [
    path('add/', add),
    path('add111/', add111)
]

image-20260318223323518

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),
]

image-20260318224106953

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

image-20260318225454085

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)
]

image-20260318230319292

注册路由时?P<page>\d+配置了参数名称和正则,所以视图可以根据page接收参数

当用实际名字接收参数时,**kwargs打印的是空字典

image-20260318230428597

如果我们不用实际名字接收参数时,**kwargs就会存实际的参数

{'page': '1'}

image-20260318230817691

无名分组和有名分组

特性 无名分组 有名分组(命名分组)
写法 (\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")

]

image-20260318231632917

8.5.1前端调用({% url "路由名称" %})

<p><a href="{% url 'home' %}">路径很全的网址</a></p>

image-20260318232016885

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

reverse("home")

image-20260318232126489

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")

image-20260319201508813

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>

image-20260319201633258

8.6.1.2位置传参(args)

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

image-20260319202013666

8.6.1.3关键字传参(kwargs)

可以通过关键字传参

image-20260319202259042

kwargs 里的 key,必须和路由中定义的参数名一模一样,否则会报错

Reverse for 'parse_name_redirect' with keyword arguments '{'name': '1'}' not found. 1 pattern(s) tried: ['parse_name_redirect/(?P<id>\\d+)/']

image-20260319203719218

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")

image-20260319203404757

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 在语义上就不成立

image-20260319203956934

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+)/']

image-20260319204325761

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

image-20260319210015772

并创建2个视图

def order(request):
    return HttpResponse("order")

def buy(request):
    return HttpResponse("buy")

image-20260319205656238

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")
]

image-20260319211005568

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

image-20260319211417965

8.7.3每个app 单独urls.py

特点:

  • 每个 app 有自己的 urls.py
  • 主路由手动导入
  • 模块化
  • 不够优雅(需要手动拼接)

每个app 单独urls.py

image-20260319212049158

然后导入到项目的urls.py

urlpatterns += shop_urlpatterns
urlpatterns += user_urlpatterns

image-20260319212131083

8.7.4include+列表

特点:

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

image-20260319212345565

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

image-20260319212953346

8.7.5每个app 独立urls.py

特点:

  • 每个 app 一个 urls.py
  • 每个文件必须有 urlpatterns
  • 主路由只负责分发
  • 最清晰
  • 最解耦
  • 企业级标准写法

配置shop/urls.pyuser/urls.py的路由,这里的路由名数组名称不能随便起了,必须是urlpatterns

image-20260319214035629

然后在项目/urls.py进行导入

urlpatterns = [
    # 方案五:每个 app 独立 urls.py(官方推荐 ⭐)
    path("shop/", include("shop.urls")),
    path("user/", include("user.urls")),
]

image-20260319214254194

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

image-20260319224144994

创建app01的视图和注册路由

def index(request):
    print(f"from app01 view : {reverse('index_view')}")
    return HttpResponse("app01 index")
    

urlpatterns = [
    path("index/", index, name="index_view")
]

image-20260319232115607

创建app02的视图和注册路由

def index(request):
    print(f"from app02 view : {reverse('index_view')}")
    return HttpResponse("app02 index")

urlpatterns = [
    path("index/", index, name="index_view")
]

image-20260319232239798

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>

image-20260319232334776

如果直接指定模块路径访问index(例如:/app01/index//app02/index/)可以访问到对应appindex路由

但是通过路由解析2个路径只能解析到 app02

image-20260319232504667

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")

image-20260319233243280

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")

image-20260319233527544

定义模版时也指定名称空间

<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>

image-20260319233625457

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

image-20260319233834269

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_namenamespace

概念 定义 位置 作用
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 只是内部验证。

image-20260320000343659

前端模版还是和以前一样,这里的app01:index_view里的app01指的是namespace

没有指定 namespace 时,模板里的 app01 就指向子 app urls.py 里的 app_name,Django 会隐式用它作为 namespace。

image-20260320000422149

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

image-20260320002057365

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")

image-20260320021240943

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")

image-20260320021914257

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")

image-20260320022004187

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")

image-20260320022032566

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")

image-20260320022105692

8.9.7自定义路径转换器

Django 的 path() 函数允许通过 注册转换器register_converter)把 URL 参数从字符串自动转换为你需要的 Python 对象。

自定义转换器主要由以下三部分组成:

属性/方法 作用
regex(必需) 一个正则表达式字符串,用于匹配 URL 中的参数。注意不能使用捕获组
to_python(self, value)(必需) URL 匹配到的字符串如何转换成 Python 对象,例如 intfloatUUID、自定义类实例等。
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  # 格式化为四位数字字符串

image-20260320022323324

注册转换器

# 【一】导入自定义的路径转换器类
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")
]

image-20260320022823874

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

image-20260320024546581

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

image-20260320025136600

9.4PyCharm创建虚拟环境

文件 ---> 设置 ---> 项目:项目名称 ---> Python解释器 ---> 添加解释器 ---> 添加本地解释器

image-20260320030045037

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

image-20260320030106652

点击确定就创建成功了

image-20260320030227954

9.5项目共享与依赖管理

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

image-20260320030504742

导出依赖文件 requirements.txt

pip freeze > requirements.txt

image-20260320030657575

安装依赖

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)

image-20260322222936962

示例

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})

image-20260322223115334

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())

image-20260322223641432

10.4表单提交

注意:

  • application/x-www-form-urlencoded: 普通表单,数据 URL 编码,Django 获取方式 request.POST,不能上传文件
  • multipart/form-data: 件上传,分块 multipart,Django 获取方式:request.POST + request.FILES
  • request.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.5CBVFBV

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>

image-20260322212336672

11.1.4函数渲染

在 Django 模板中,函数的渲染逻辑 与 Python 普通调用不同,需要注意以下规则:

  • 有参函数 → 必须在视图里调用

  • 模板只会调用“无参函数/方法”。模板中写 {{ func }},如果 func 是无参函数,它会被自动调用

  • 函数有返回值 → 渲染返回值

  • 函数无返回值(返回 None) → 渲染 None

示例代码见数据类型渲染

image-20260322213040065

11.1.5类和对象的渲染

结论

  • 类本身访问方法 → 模板会执行(只要是无参 callable,包括类方法和静态方法)
  • 对象访问方法 → 模板不会执行(因为绑定了 self,模板无法提供)

模板只会自动执行“无参 callable”,类的方法是无参的,绑定到对象的方法是有 self 的 → 不执行

示例代码见数据类型渲染

image-20260322214227385

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>" → &lt;h1&gt;hello&lt;/h1&gt;\

方法一:模板前端 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>

image-20260322220117244

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>

image-20260322220800113

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>

image-20260325233646779

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()

image-20260325234109790

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())

image-20260325234226372

前端导入CommonInclusionTag,然后调用传入info_data参数

{% load CommonInclusionTag %}

 {% adv_temp  info_data %}

image-20260325234421418

总结

步骤 内容
创建包 在 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,拆分了headtitle、网站头部、导航条、页面内容、页脚

<!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>

image-20260326000407607

image-20260326000854547

定义子页面,使用extends 继承base.html,重写了title、页面内容、页脚,其他复用

{% extends "base.html" %}

{% block title %}文章页面{% endblock %}

{% block content %}
<h2>文章标题</h2>
<p>这里是文章内容</p>
{% endblock %}

{% block footer %}
<p>自定义页脚 © 2026</p>
{% endblock %}

image-20260326000744661

image-20260326000953908

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

image-20260326001418735

12.1.2导入模型表

所有的模型表都在models.py里面,提前导入模型表

 from user.models import User

image-20260326001911872

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',                          # 数据库端口
    }
}

image-20260326001944583

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'

image-20260316230252862

第一种方式: 安装mysqlclient,官方推荐

直接安装mysqlclient,官方推荐,我使用的就是这种方式

pip install mysqlclient

image-20260316230450531

第二种方式安装: PyMySQL

安装PyMySQL,我这里记录一下这种方式

pip install mysqlclient

项目的 __init__ 文件进行导入

import pymysql
pymysql.install_as_MySQLdb()

image-20260318173243069

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

image-20260326002209448

12.1.6数据库迁移

执行数据库迁移命令的时候注意自己的虚拟环境

# 生成迁移记录
python manage.py makemigrations
# 执行迁移
python manage.py migrate

image-20260326002512440

也可以使用manage.py执行,这样就不用输入python manage.py

# 生成迁移记录
makemigrations
# 执行迁移
migrate

image-20260326002804919

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 语句

image-20260401225949894

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

image-20260401230013194

12.4数据库表关系

12.4.1简介

Django 中三种主要表关系:

关系类型 Python / Django 实现 场景示例 说明
一对一(OneToOne) OneToOneFieldForeignKey(..., unique=True) 用户 ↔ 用户详情 每个用户只有一个详情,每条详情只对应一个用户
一对多(ForeignKey) ForeignKey 部门 ↔ 员工 多个员工属于同一个部门,但每个员工只属于一个部门
多对多(ManyToMany) ManyToManyField 或手动建中间表 作者 ↔ 图书 一个作者可以写多本书,一本书也可以有多个作者

注意:

  • 外键(ForeignKey)总是指向 “多” 端的表。
  • 多对多关系需要 第三张表 来存储关联关系。

12.4.2手动创建第三张表实现多对多

多对多关系(Many-to-Many)

  • 一个作者可以写多本书
  • 一本书可以有多个作者

手动创建中间表BookToAuthor1

  • Django 会自动创建中间表,但你可以显式定义
  • 优势:可以在中间表增加额外字段,例如 create_timerole(作者角色)等
  • 缺点:需要手动管理关联,操作稍微复杂一些
# ======================================================
# 【二】方案一:手动创建第三张表(显式管理多对多) +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"

image-20260401230238334

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"

image-20260401230307509

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"

image-20260401230325450

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

image-20260326235935665

复制admin模块的绝对路径

image-20260327000027666

定位到 admin 模块的 migrations 文件夹

image-20260327000138705

删除 migrations 文件夹下 除了 init.py 以外的所有文件

再重新输入迁移命令

image-20260327000244622

总结

# 删除数据库中不想要的第三张表/某一张表
# 再次迁移发现 迁移不成功 每次都提示 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))

image-20260401171843016

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}")

image-20260401172041068

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")

image-20260401172213214

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}")

image-20260401173514193

12.5多表查询

Django 多表查询本质就一句话:正向查字段,反向查表名小写_set(默认)

关系 & 查询方式总览

关系类型 正向查询 反向查询
一对多(ForeignKey) obj.字段 obj.子表_set
多对多(ManyToMany) obj.字段.all() obj.表_set.all()
一对一(OneToOne) obj.字段 obj.表名

基本原则

查询方向 含义 用法
正向查询 拥有字段的模型 去查 直接用字段名
反向查询 被引用的模型 去查 用默认 _setrelated_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()]}")

image-20260401212144292

12.6基于下划线的夸表查询

注意点

  • 正向查询永远使用模型字段名,不用模型类名。
  • 反向查询要用 Django 默认生成的反向属性:
    • ForeignKey / ManyToManyField 默认 _set
    • OneToOneField 默认小写模型名
  • 多级查询:可以连续使用 __
    • author__detail__phoneauthor__book__id
  • values() 返回字典列表,适合只取部分字段,节省性能。
  • 注意 N+1 问题:大数据量时,多级正向查询可以用 select_relatedprefetch_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))

image-20260401220303195

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']}")

image-20260401220643900

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))

image-20260401221114054

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
    }
}

image-20260401222435650

工作原理

每个请求自动包裹成一个事务:

请求开始 → 开启事务
请求结束 → 自动提交
发生异常 → 自动回滚

优点

  • 不需要手动写事务
  • 简单方便

缺点

  • 性能开销大
  • 所有请求都开启事务(不必要)
  • 不适合高并发系统

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/已经取消了全局事务特性,就算代码有异常(数值转换失败),也依然会成功添加数据

image-20260401224623965

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)

image-20260401224847359

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()  # 显示支付方式的名称

image-20260401231208672

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())

image-20260411222236933

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())

image-20260411222352235

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)

image-20260411223724018

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()  # 保存到数据库

image-20260411223912124

12.13.3视图返回

serializers.serialize 返回 JSON 字符串,而 JsonResponse 负责将 Python 对象转换为 JSON,因此不能直接嵌套使用,否则会导致双重序列化问题。

safe=FalseJsonResponse 里的一个安全限制开关,核心作用非常明确:允许返回非 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)

image-20260411224500519

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>

image-20260411225909064

12.15Djangoform组件

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>

image-20260413210743766

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

image-20260413212010085

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})

image-20260413212128972

代码:

  • 模拟usernamepassword长度错误
  • 局部钩子clean_username验证username必须nb_开头
  • 全局钩子clean验证passwordconfirm_password必须一致

image-20260413212748687

执行流程

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

image-20260413215256112

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})

image-20260413215416680

13.DjangoCookieSession

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 是服务器写入浏览器的一段键值对数据,用于维持用户登录状态,每次请求会自动携带。

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("未登录请先登陆")
# 【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.2DjangoSession 依赖数据库

使用 session 需要:

  • 1.配置 settings.py\INSTALLED_APPS 中的 django.contrib.sessions

  • 2.执行数据库迁移:python manage.py migrate

  • 3.系统会自动创建表:django_session

配置 django.contrib.sessions

image-20260416181514637

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

makemigrations
migrate

image-20260416181852648

django_session

image-20260416182104147

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.3CookieSession的示例

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/

image-20260416183552056

如果未登录就不能访问

image-20260416183655586

14.DjangoCSRF装饰器使用方法

14.1CSRF装饰器简介

CSRF(Cross Site Request Forgery)跨站请求伪造

Django 默认开启 CSRF 防护:

  • 针对 POST / PUT / DELETE 请求
  • 防止恶意站点伪造用户请求

如果关闭中间件:

  • MIDDLEWARE 中注释掉 'django.middleware.csrf.CsrfViewMiddleware',则 POST 请求会直接报错。

POST请求会报错Forbidden (403)

image-20260416212157752

Django 提供两个核心装饰器:

  • from django.views.decorators.csrf import csrf_protect, csrf_exempt
  • from django.utils.decorators import method_decorator

Django 的 CSRF 机制只校验这些“不安全方法”:

  • POST
  • PUT
  • PATCH
  • DELETE

而以下被认为是“安全方法”,不会触发 CSRF 校验

  • GET
  • HEAD
  • OPTIONS
  • TRACE

开启或关闭用法都是一样的,我这里不分开写了,开启是@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("登录成功")

image-20260416212632707

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 支持(缓存优化)
    • 处理一些基础请求校验
  • 请求基础整理 + 规范化

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 劫持攻击(钓鱼页面嵌套)
]

image-20260416214810085

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',
]

image-20260416221003998

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

image-20260416221307571

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,只用于测试,不推荐生产

image-20260416225553706

方案二:类装饰器或者方法装饰器

# 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(...)
"""

image-20260416225336506

方案三:在 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">

image-20260416225450886

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>
"""

image-20260416224111557

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>

image-20260416225852562

我这里只演示导入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>

image-20260416230325694

16.5总结

CSRF 是利用浏览器自动携带 Cookie 的机制,伪造用户请求的一种攻击方式。

Django 防御核心

  • CSRF Token
  • Cookie + Token 双重校验
  • CsrfViewMiddleware

开发建议

  • 表单必须加 {% csrf_token %}
  • Ajax 必须带 token
  • 不要随意使用 csrf_exempt

17.Djangoauth模块

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

image-20260416232719437

核心思想(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

首先要完成所有模块的迁移,我这里不知道为什么不能自动迁移所有模块,需要手动迁移自定义模块useradmin模块

然后输入用户名和密码,我这里设置的是admin/admin,中间跳过密码验证即可

image-20260417000205933

然后访问后台登录

http://127.0.0.1:8000/admin

image-20260417000730799

auth_user表结构总览

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

auth_user

image-20260417002627342

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} 创建成功!")

image-20260417002804529

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} 修改密码成功!")

image-20260417003936355

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="性别")

image-20260417004302503

settings.py配置AUTH_USER_MODEL

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

image-20260417004352522

数据库迁移(如果不成功,建议把之前的数据库和迁移文件删了,正常来说一开始也不会直接使用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} 创建成功!")

image-20260417010127658

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

微信打赏

posted @ 2026-06-09 01:22  peng_boke  阅读(2)  评论(0)    收藏  举报