Django介绍
Django,发音为['dʒæŋɡəʊ]
,Django诞生于2003年秋天,2005年发布正式版本,由Simon和Andrian开发。Django具有以下特点:
快速开发。Django是一个开箱即用的Web框架,包括了数据库处理、HTML渲染、Admin系统、发送邮件、登录鉴权等。由于集成了许多功能,所以只要你学会了,以后开发的速度是非常快的。
安全性高。Django在安全方面做得非常完善,节省开发者处理安全的时间。比如SQL注入、CSRF攻击,点击劫持等常见的Web安全问题,Django都已经处理好了。
可伸缩性强。世界上很多大型网站都是用Django开发的,能快速和灵活的调整硬件来满足不同的流量需求。
Github源代码:https://github.com/django/django
Django官网:https://www.djangoproject.com
安装Django:使用pip或者conda命令安装
URL组成部分详解
URL 是 Uniform Resource Locator 的简写,统一资源定位符。
一个 URL 由以下几部分组成:scheme://host:port/path/?query-string=xxx#anchor
- scheme:代表的是访问的协议,一般为 http 或者 https 以及 ftp 等。
- host:主机名,域名,比如 www.baidu.com 。
- port:端口号。HTTP协议是80端口,HTTPS协议是443端口。
- path:查找路径。比如: www.jianshu.com/trending/now ,后面的 trending/now 就是 path 。
- query-string:查询字符串,比如: www.baidu.com/s?wd=python ,后面的 wd=python 就是查询字符串。
- anchor:锚点,后台一般不用管,前端用来做页面定位的。
注意: URL 中的所有字符都是 ASCII 字符集,如果出现非 ASCII 字符,比如中文,浏览器会进行编码再进行传输。
创建Django项目
用命令行的方式:打开终端,使用命令: django-admin startproject [项目名称]
即可创建。比如:django-admin startproject first_project
。 以下是项目结构:
first_project/ # 项目根目录(你创建项目时指定的名字)
├── manage.py # 用于与项目交互的命令行工具(如运行服务器、迁移数据库等)
└── first_project/ # 实际的 Django 项目代码(和项目同名的子目录)
├── __init__.py # 标识该目录为一个 Python 包
├── asgi.py # ASGI 网关接口配置(用于异步部署)
├── settings.py # 项目的配置文件(数据库、app、静态文件等)
├── urls.py # URL 路由配置
└── wsgi.py # WSGI 网关接口配置(用于同步部署)
manage.py
:以后和项目交互基本上都是基于这个文件。一般都是在终端输入 python manage.py [子命令]
。可以输入 python manage.py help
看下能做什么事情。除非你知道你自己在做什么,一般情况下不应该编辑这个文件。
settings.py
:本项目的设置项,以后所有和项目相关的配置都是放在这个里面。
urls.py
:这个文件是用来配置URL路由的。比如访问 http://127.0.0.1/news/ 是访问新闻列表页,这些东西就需要在这个文件中完成。
wsgi.py
:项目与 WSGI 协议兼容的 web 服务器入口,部署的时候需要用到的,一般情况下也是不需要修改的。
Django 约定将项目外层容器和内部代码包命名一致,这种约定使得新手和团队开发都能迅速理解项目结构。同时也遵循 Python 模块的最佳实践,让整个工程在设计上更规范。
django-admin startproject LinNote .
:
- 注意末尾这个
.
是关键,它的作用是:**在当前目录下创建 Django 项目结构,而不是额外再套一层目录
创建APP
app 是 django 项目的组成部分。一个 app 代表项目中的一个模块,所有 URL 请求的响应都是由 app 来处理。比如豆瓣,里面有图书,电影,音乐,同城等许许多多的模块,如果站在 django 的角度来看,图书,电影这些模块就是 app ,图书,电影这些 app 共同组成豆瓣这个项目。因此这里要有一个概念,django 项目由许多 app 组成,一个 app 可以被用到其他项目, django 也能拥有不同的 app 。
通过命令创建模块:
python manage.py startapp book
创建的目录情况:
book/
├── migrations/ # 数据库迁移文件目录(自动生成)
│ └── __init__.py # 标识目录为Python包
├── __init__.py # 标识book为Python包
├── admin.py # 配置Django后台管理界面
├── apps.py # 应用配置(如应用名称)
├── models.py # 定义数据模型(数据库表结构)
├── tests.py # 编写单元测试
└── views.py # 处理业务逻辑(视图函数/类)
URL分发器
视图View
视图一般都写在 app
的 views.py
中。并且视图的第一个参数永远都是 request
(一个HttpRequest
)对象。这个对象存储了请求过来的所有信息,包括携带的参数以及一些头部信息等。
在视图中,一般是完成逻辑相关的操作。比如这个请求是添加一篇博客,那么可以通过request
来接收到这些数据,然后存储到数据库中,最后再把执行的结果返回给浏览器。视图函数的返回结果必须是HttpResponseBase
对象或者子类的对象。
示例代码如下:
from django.http import HttpResponse
def book_list(request):
return HttpResponse("书籍列表!")
视图可以是函数,也可以是类,我们先学习函数视图,后面再学习类视图
URL映射
视图写完后,要与URL
进行映射,也即用户在浏览器中输入什么 url 的时候可以请求到这个视图函数。在用户输入了某个 url
,请求到我们的网站的时候, django
会从项目的 urls.py
文件中寻找对应的视图。在 urls.py
文件中有一个 urlpatterns
变量,以后 django
就会从这个变量中读取所有的匹配规则。匹配规则需要使用 django.urls.path
函数进行包裹,这个函数会根据传入的参数返回URLPattern
或者是 URLResolver
的对象。示例代码如下:
from django.contrib import admin
from django.urls import path
from book import views
urlpatterns = [
path('admin/', admin.site.urls),
path('book/',views.book_list)
]
URL中添加参数
Path函数方式
有时候, url 中包含了一些参数需要动态调整。比如简书某篇文章的详情页的url,是https://www.jianshu.com/p/a5aab9c4978e
后面的 a5aab9c4978e 就是这篇文章的 id ,那么简书的文章详情页面的url就可以写成https://www.jianshu.com/p/<id>
,其中id就是文章的id。
那么如何在django 中实现这种需求呢。这时候我们可以在 path 函数中,使用尖括号的形式来定义一个参数。
比如我现在想要获取一本书籍的详细信息,那么应该在 url 中指定这个参数。示例代码如下
from django.contrib import admin
from django.urls import path
from book import views
urlpatterns = [
path('admin/', admin.site.urls),
path('book/', views.book_list),
path('book/<book_name>/<book_id>',views.book_detail)
]
而 views.py
中的代码如下:book_id,book_name
参数名称是根据path函数('book/<book_name>/<book_id>'
)定义的。
def book_detail(request,book_id,book_name):
text = f"您输入的书籍的id是:{book_id},书名是{book_name}。"
return HttpResponse(text)
访问:http://localhost:8000/book/红楼梦/11
在指定参数时,也可以指定参数的类型,比如以上 book_id 为整形,那么在定义 URL 的时候,就可以使用以下语法实现:
from django.contrib import admin
from django.urls import path
from book import views
urlpatterns = [
path('admin/', admin.site.urls),
path('book/', views.book_list),
path('book/<book_name>/<int:book_id>',views.book_detail)
]
除了 int
类型, django 的 path 部分还支持str
、slug
、uuid
、path
类型
指定类型的好处:
- 如果访问的不是整数类型,如
http://localhost:8000/book/红楼梦/11f
,会报404 - 由于在指定了参数类型,那么在接受参数的时候,可以用整数类型的变量去接收。避免了类型转换。
- 在不指定类型的情况下,默认所有参数类型都是
str
查询字符串的方式
也可以通过查询字符串的方式传递一个参数过去。代码如下:
from django.contrib import admin
from django.urls import path
from book import views
urlpatterns = [
path('admin/', admin.site.urls),
path('book/', views.book_list),
path('book/<int:book_name>/<book_id>',views.book_detail),
path('book/detail',views.book_detail2)
]
而 views.py
中的代码如下
def book_detail2(request):
# 如何字典里面不存在 id,就会报错
#book_id = request.GET.get["id"]
# 如果字典里面不存在id,返回null
book_id = request.GET.get("id")
book_name = request.GET.get("name")
text = f"您输入的书籍的id是:{book_id},书名是{book_name}。"
return HttpResponse(text)
访问:http://localhost:8000/book/detail?name=“红楼梦”&id=11
path函数
path 函数的定义为: path(route,view,name=None,kwargs=None)
。以下对这几个参数进行讲解。
route
参数: url 的匹配规则。这个参数中可以指定 url 中需要传递的参数,比如在访问文章详情页的时候,可以传递一个 id 。传递参数是通过<>
尖括号来进行指定的。并且在传递参数的时候,可以指定这个参数的数据类型,比如文章的 id 都是 int 类型,那么可以这样写<int:id>
,以后匹配的时候,就只会匹配到 id 为 int 类型的 url ,而不会匹配其他的 url ,并且在视图函数中获取这个参数的时候,就已经被转换成一个 int 类型了。其中还有几种常用的类型:str
:非空的字符串类型。默认的转换器。但是不能包含斜杠。int
:匹配任意的零或者正数的整形。到视图函数中就是一个int类型。slug
:由英文中的横杠-
,或者下划线_
连接英文字符或者数字而成的字符串(不能是中文,否则404)。uuid
:匹配 uuid 字符串。path
:匹配非空的英文字符串,可以包含斜杠/
def book_path(request,book_id,book_name): text = f"您输入的书籍的id是:{book_id},书名是{book_name}。" return HttpResponse(text) from django.contrib import admin from django.urls import path from book import views urlpatterns = [ path('book/path/<path:book_name>/<int:book_id>',views.book_path), ]
当访问:
http://localhost:8000/book/path/a/b/11/123
结果是:您输入的书籍的id是:123,书名是a/b/11。
也就是会默认把最后一个匹配成
book_id
,其他的匹配成book_name
view
参数:可以为一个视图函数或者是 类视图.as_view()
或者是django.urls.include()
函数的返回值。name
参数:这个参数是给这个 url 取个名字的,这在项目比较大, url 比较多的时候用处很大。kwargs
(传给 view 的额外参数,可选)。类型:dict
。作用:向视图函数传递额外的关键字参数(不常用)。path('books/', views.book_list, kwargs={'category': 'novel'})
在
views.book_list(request, category)
里就能收到category='novel'
路由模块化
在我们的项目中,不可能只有一个 app ,如果把所有的 app 的 views 中的视图都放在 urls.py
中进行映射,肯定会让代码显得非常乱。因此 django 给我们提供了一个方法,可以在 app 内部包含自己的 url 匹配规则,而在项目的 urls.py
中再统一包含这个 app 的 urls 。
使用这个技术需要借助 include
函数。示例代码如下:
App的视图函数views.py
:
from django.http import HttpResponse
def movie_list(request):
return HttpResponse("电影列表!")
def movie_detail(request,movie_id):
text = f"您输入的电影的id是:{movie_id}。"
return HttpResponse(text)
APP 的urls.py
:
from django.urls import path
from . import views
# 指定应用命名空间
# 为了避免多个模块的 urls.py 中包含同名的 url ,可以指定一个应用命名空间
# 便于反向解析 URL
# - 在 Python 代码中使用 reverse('命名空间:路由名称')、
# - 在模板中使用 {% url '命名空间:路由名称' %}
app_name='movie_space'
urlpatterns = [
path('list/',views.movie_list),
path('detail/<movie_id>/',views.movie_detail)
]
Project的urls.py
: 最终访问的url是:Project的urls.py
+APP 的urls.py
= movie/
+detail/<movie_id>/
from django.contrib import admin
# 这里引入 include 函数
from django.urls import path,include
from book import views
urlpatterns = [
path('movie/',include("movie.urls")) # include(包名.文件名)
]
url反转
url反转就是:通过视图的 name(别名)自动生成对应的 URL 地址。这样以后修改了路径,只需要改 urls.py
,模板或代码中都不用动!
URL 反转的常用方式:
- 在模版中,使用
{% url %}
模板标签: - 在代码中,使用
reverse()
函数:
reverse()
函数的使用:
只使用名字:没有设置
app_name
,直接在主urls.py
中定义了name=xxx
,就不需要命名空间。from django.contrib import admin from django.urls import path,include from book import views urlpatterns = [ path('admin/', admin.site.urls), # 定义在 主 urls.py path('book/', views.book_list, name='book_list'), ] from django.http import HttpResponse # 导入 reverse from django.urls import reverse def book_list(request): # 返回:/book/ print(reverse("book_list")) return HttpResponse("书籍列表!")
如果有应用命名空间或者有实例命名空间,那么应该在反转的时候加上命名空间
from django.contrib import admin from django.urls import path,include from book import views urlpatterns = [ path('movie/',include("movie.urls")) # include(包名.文件名) ] from django.urls import path from . import views # 指定应用命名空间 app_name='movie_space' urlpatterns = [ path('list/',views.movie_list,name='movie_list'), ] from django.http import HttpResponse # 导入 reverse from django.urls import reverse def book_list(request): # 返回:/movie/list/ print(reverse("movie_space:movie_list")) return HttpResponse("书籍列表!")
如果这个url中需要传递参数,那么可以通过 kwargs 来传递参数。
from django.contrib import admin from django.urls import path,include from book import views urlpatterns = [ path('movie/',include("movie.urls")) # include(包名.文件名) ] from django.urls import path from . import views # 指定应用命名空间 app_name='movie_space' urlpatterns = [ path('detail/<movie_id>/',views.movie_detail,name='movie_detail') ] from django.http import HttpResponse # 导入 reverse from django.urls import reverse def book_list(request): # 返回:/movie/detail/1/ # kwargs={"movie_id": 1} 中的参数名必须好url的参数名保持一致 print(reverse("movie_space:movie_detail", kwargs={"movie_id": 1})) return HttpResponse("书籍列表!")
因为 django 中的
reverse
反转 url 的时候不区分GET
请求和POST
请求,因此不能在反转的时候添加查询字符串的参数。如果想要添加查询字符串的参数,只能手动的添加。def book_list(request): # 返回:/book/detail?name=“红楼梦”&id=11 print(reverse("book_detail2")+"?name=“红楼梦”&id=11") return HttpResponse("书籍列表!") from django.contrib import admin from django.urls import path,include from book import views urlpatterns = [ path('book/detail',views.book_detail2,name='book_detail2'), ]
settings.py:DEBUG = True
在 Django 的 settings.py
文件中DEBUG = True
的意思是:开启调试模式,适用于开发阶段。
开启 DEBUG = True 时,会发生以下行为:
- 详细错误页面:如果你的网站出错了(比如视图报错、模板出错等),Django 会显示非常详细的错误信息,包括堆栈信息、出错的代码、变量值等,非常适合调试用。
- 自动重新加载:当你修改 Python 代码(比如 views、models)后,服务器会自动重启,无需手动重启。
- 静态文件自动处理:开发阶段,Django 会自动处理和提供静态文件(
static/
)。
但是在生产环境中,必须设置为:DEBUG = False
。否则:
- 用户可能看到详细的错误信息,泄露服务器敏感数据(如数据库配置、路径等)。
- 你的网站的安全性会大大降低。
在DEBUG = False
之后,还需要配置另一参数:ALLOWED_HOSTS
。否则请求会被拒绝。如下图:
CommandError: You must set settings.ALLOWED_HOSTS if DEBUG is False.
ALLOWED_HOSTS
可以设置为相关的IP或者域名:
ALLOWED_HOSTS = ['example.com', 'www.example.com']
如果把DEBUG = False
之后,还想在本地启动,可以作如下配置:
DEBUG = False
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
模板
DTL
DTL 是 Django Template Language 三个单词的缩写,也就是Django自带的模板语言。当然也可以配置Django支持Jinja2等其他模板引擎,但是作为Django内置的模板语言,和Django可以达到无缝衔接而不会产生一些不兼容的情况。因此建议大家学习好DTL。
DTL模板是一种带有特殊语法的HTML文件,这个HTML文件可以被Django编译,可以传递参数进去,现数据动态化。在编译完成后,生成一个普通的HTML文件,然后发送给客户端。
渲染模板
render_to_string
:找到模板,然后将模板编译后渲染成Python的字符串格式。最后再通过HttpResponse
类包装成一个 HttpResponse
对象返回回去
from django.shortcuts import render
from django.template.loader import render_to_string
from django.http import HttpResponse
def book_index(request):
# 将模板编译后渲染成Python的字符串格式
html = render_to_string("index.html")
# 字符串:<class 'django.utils.safestring.SafeString'>
print(type(html))
return HttpResponse(html)
from django.contrib import admin
from django.urls import path
from book import views
urlpatterns = [
path('admin/', admin.site.urls),
path("index/", views.book_index, name="index"),
]
以上方式虽然已经很方便了。但是django还提供了一个更加简便的方式,**直接将模板渲染成字符串和包装成 HttpResponse
对象一步到位完成。**示例代码如下:
from django.shortcuts import render
def book_index(request):
# html = render_to_string("index.html")
# # 字符串:<class 'django.utils.safestring.SafeString'>
# print(type(html))
# return HttpResponse(html)
return render(request,"index.html")
模板查找路径配置
在项目的 settings.py
文件中。有一个 TEMPLATES
配置,这个配置包含了模板引擎的配置,模板查找路径的配置,模板上下文的配置等。
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'] # 要检查这里是否配置
,
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
BACKEND
:指定你用的模板引擎。这个是 Django 自带的模板系统,也叫 DjangoTemplates。也可以用 'django.template.backends.jinja2.Jinja2'
来用 Jinja2 模板。
DIRS
:这是一个列表,在这个列表中可以存放所有的模板(HTML)路径,以后在视图中使用 render
或者render_to_string
渲染模板的时候,会在这个列表的路径中查找模板。BASE_DIR
是项目的根目录,所以这个表示:项目根目录/templates/
。
APP_DIRS
:默认为 True
。这个设置为 True
后,Django会根据 INSTALLED_APPS
的相关配置,找到相关的 APP
,再找到对应的templates
文件夹,去里面查找模板(HTML)。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'book.apps.BookConfig', # 这是我在用Pycharm创建项目的时候创建的book app,所以这里有这个配置
]
如果,我后期创建一个movie app,在movie app里面创建了一个文件夹templates,里面创建了一个movie.html
可是我们没有配置INSTALLED_APPS
,这个时候,我们在views.py
里面写代码的时候,就会找不到movie.html
这个文件。
为此我们需要加上movie app:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'book.apps.BookConfig',
'movie.apps.MovieConfig', # 新增movie app
]
相关代码如下:
# movie.views.py
from django.shortcuts import render
def movie_list(request):
return render(request, "movie.html")
# Project 的 urls.py
from django.contrib import admin
from django.urls import path
# 注意,这里有两个同名的views,所以要取别名
from movie import views as movie_views
from book import views as book_views
urlpatterns = [
path('admin/', admin.site.urls),
path("index/", book_views.book_index, name="index"),
path("book/list", book_views.book_list, name="list"),
path("movie/list", movie_views.movie_list, name="movie_list"),
]
OPTIONS
:这部分是对模板引擎的附加设置。
查找顺序:比如代码 render('index.html')
。
读取
settings.py
中的TEMPLATES
设置TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'templates'], # 手动指定的模板目录 'APP_DIRS': True, # 启用自动查找每个 app 的 templates 文件夹 ... } ]
先查
DIRS
里你手动指定的路径- 默认通常是
[BASE_DIR / 'templates']
- 即项目根目录下的
templates/
文件夹(全局模板目录) - 所以如果你在
templates/index.html
放了模板,这里就能找到
- 默认通常是
如果
APP_DIRS=True
,会继续查找各个 App 内部的模板目录。如果多个 app 都有同名模板,比如多个index.html
,Django 会按照 App 的加载顺序(在INSTALLED_APPS
中的顺序)优先匹配先出现的那个 app 的模板。
变量
模板中可以包含变量, Django 在渲染模板的时候,可以传递变量对应的值过去进行替换。
变量的命名规范和 Python 非常类似,只能是阿拉伯数字和英文字符以及下划线的组合,不能出现标点符号等特殊字符。
变量需要通过视图函数渲染,视图函数在使用 render
或者render_to_string
的时候可以传递一个**context
** 的参数,这个参数是一个字典类型。以后在模板中的变量就从这个字典中读取值的。示例代码如下
def info(request):
# 1. 普通的变量
username = '令狐冲'
# 2. 字典类型
book = {'name': "水浒传", 'author': '施耐庵'}
# 3. 列表
books = [
{'name': "水浒传", 'author': '施耐庵'},
{'name': "三国演义", 'author': '罗贯中'}
]
# 4. 对象
class Person:
def __init__(self, realname):
self.realname = realname
context = {
'username': username,
'book': book,
'books': books,
'person': Person("令狐冲")
}
return render(request, 'info.html', context=context)
在HTML里,可以作如下提取:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>信息</title>
</head>
<body>
<p>{{ username }}</p>
<p>图书名称:{{ book.name }}</p>
<!-- 这里的 book 是一个列表,下标从0开始。下标为1的,就对应: {'name': "水浒传", 'author': '施耐庵'} -->
<p>下标为1图书的名称:{{ books.1.name }}</p>
<p>姓名为:{{ person.realname }}</p>
</body>
</html>
常用的模板标签
官方文档:https://docs.djangoproject.com/en/5.0/ref/templates/builtins/
if 标签
if 标签相当于 Python 中的 if 语句,有 elif
和 else
相对应,但是所有的标签都需要用标签符号( {%%}
)进行包裹。
if 标签中可以使用 ==、!=、<、<=、>、>=、in、not in、is、is not
等判断运算符。示例代码如下:
# urls.py
from django.contrib import admin
from django.urls import path
from movie import views as movie_views
from book import views as book_views
urlpatterns = [
path("book/tag", book_views.tag, name="movie_tag"),
]
# views.py
def tag(request):
age = 18
context = {
'age': age,
}
return render(request, 'tag.html', context=context)
# tag.html
{% if age > 18 %}
<p> 您已满18岁,可以进入网吧 </p>
{% elif age == 18 %}
<p> 请在等等 </p>
{% else %}
<p> 您不满18岁 </p>
{% endif %}
for…in…
for...in...
类似于 Python 中的 for...in...
。可以遍历列表、元组、字符串、字典等一切可以遍历的对象。
# views.py
def tag(request):
age = 18
books = [
{"name":"三国演义","author":"施耐庵"},
{"name":"活着","author":"余华"}
]
context = {
'age': age,
'books': books,
}
return render(request, 'tag.html', context=context)
# tag.html
<H1>for 标签</H1>
<table>
<thead>
<tr>
<th>书籍</th>
<th>作者</th>
</tr>
</thead>
<tbody>
{% for book in books %}
<tr>
<td>{{book.name}}</td>
<td>{{book.author}}</td>
</tr>
{% endfor %}
</tbody>
</table>
如果想要反向遍历,那么在遍历的时候就加上一个reversed
<table>
<thead>
<tr>
<th>书籍</th>
<th>作者</th>
</tr>
</thead>
<tbody>
# 这里放入 reversed 标签
{% for book in books reversed %}
<tr>
<td>{{book.name}}</td>
<td>{{book.author}}</td>
</tr>
{% endfor %}
</tbody>
</table>
遍历字典的时候,需要使用 items
、 keys
和 values
等方法。在 DTL
中,执行一个方法不能使用圆括号的形式。
{% for key,value in person.items %}
<p>key: {{ key }}</p>
<p> value: {{ value }}</p>
{% endfor %}
def tag(request):
person_dic={
"name":"张三",
"age":18,
"height":180
}
context = {
'person': person_dic
}
return render(request, 'tag.html', context=context)
在 for 循环中, DTL 提供了一些变量可供使用。这些变量如下:
forloop.counter
:当前循环的下标。以1作为起始值。forloop.counter0
:当前循环的下标。以0作为起始值。forloop.revcounter
:当前循环的反向下标值。比如列表有5个元素,那么第一次遍历这个属性是等于5,第二次是4,以此类推。并且是以1作为最后一个元素的下标。forloop.revcounter0
:类似于forloop.revcounter
。不同的是最后一个元素的下标是从0开始。forloop.first
:是否是第一次遍历。forloop.last
:是否是最后一次遍历。forloop.parentloop
:如果有多个循环嵌套,那么这个属性代表的是上一级的for循环。
<ul>
{% for book in books %}
<li>
{{ forloop.counter }}. {{ book.name }} - 作者:{{ book.author }}
{% if forloop.first %}<strong>(第一本)</strong>{% endif %}
{% if forloop.last %}<strong>(最后一本)</strong>{% endif %}
</li>
{% endfor %}
</ul>
<table>
<tr>
<th>counter</th>
<th>counter0</th>
<th>revcounter</th>
<th>revcounter0</th>
<th>first?</th>
<th>last?</th>
<th>书名</th>
</tr>
{% for book in books %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ forloop.counter0 }}</td>
<td>{{ forloop.revcounter }}</td>
<td>{{ forloop.revcounter0 }}</td>
<td>{{ forloop.first }}</td>
<td>{{ forloop.last }}</td>
<td>{{ book.name }}</td>
</tr>
{% endfor %}
</table>
for…in…empty
这个标签使用跟 for...in...
是一样的,只不过是在遍历的对象如果没有元素的情况下,会执行 empty
中的内容。示例代码如下:
{% for person in persons %}
<li>{{ person }}</li>
{% empty %}
暂时还没有任何人
{% endfor %}
with
在模版中定义变量。有时候一个变量访问的时候比较复杂,那么可以先把这个复杂的变量缓存到一个变量上,以后就可以直接使用这个变量就可以了。示例代码如下:
{% with a=books.1 %}
<p>{{ a }}</p>
{% endwith %}
在 with
语句中定义的变量,只能在 {%with%}
{%endwith%}
中使用,不能在这个标签外面使用。
定义变量的时候,不能在等号左右两边留有空格。比如{% with lisi = persons.1%}
是错误的。
books.1
:这是 用点语法访问列表中的索引,DTL 中没有 books[1]
这样的写法,只能用 books.1
还有另外一种写法同样也是支持的:
{% with books.1 as a %}
<p>{{ a }}</p>
{% endwith %}
url 标签
在模版中,我们经常要写一些 url ,比如某个 a 标签中需要定义 href 属性。
当然如果通过硬编码的方式直接将这个 url 写死在里面也是可以的。但是这样对于以后项目维护可能不是一件好事。
因此建议使用这种反转的方式来实现,类似于 django 中的 reverse 一样。示例代码如下:
# 项目的urls.py
path("book/list", book_views.book_list, name="list")
# tag.html
<a href="{% url 'list' %}">图书列表页面</a>
# 如果url是定义在APP里面,那么还需要加上命名空间(比如:app_name='book')
# book:list= app_name:url_name
<a href="{% url 'book:list' %}">图书列表页面</a>
经过渲染之后,变成了/book/list
如果 url 反转的时候需要传递参数,那么可以在后面传递。但是参数分位置参数和关键字参数。位置参数和关键字参数不能同时使用。
示例代码如下:
# tag.html
# url反转,使用位置参数
<a href="{% url 'book_detail' 1 %}">图书详情页面</a>
# url反转,使用关键字参数
<a href="{% url 'book_detail' book_id=1 %}">图书详情页面</a>
#urls.py
path("book/detail/<int:book_id>", book_views.book_detail, name="book_detail"),
# views.py
def book_detail(request, book_id):
books = [
{"name": "三国演义", "author": "施耐庵"},
{"name": "活着", "author": "余华"},
{"name": "三体", "author": "刘慈欣"}
]
book = books[book_id]
return render(request, 'detail.html', context={"book":book})
如果想要在使用 url 标签反转的时候要传递查询字符串的参数,那么必须要手动在在后面添加。
<a href="{% url 'book_detail' book_id=1 %}?page=1">图书详情页面</a>
如果需要传递多个参数,那么通过空格的方式进行分隔。示例代码如下:
<a href="{% url 'book_detail2' book_id=1 page=2 %}">图书详情页面5</a>
path("book/detail/<int:book_id>/<int:page>", book_views.book_detail, name="book_detail2"),
spaceless
spaceless 标签:移除html标签中的空白字符。包括空格、tab键、换行等。示例代码如下:
{% spaceless %}
<p>
<a href="foo/">Foo</a>
</p>
{% endspaceless %}
那么在渲染完成后,会变成以下的代码:
<p><a href="foo/">Foo</a></p>
spaceless 只会移除html标签之间的空白字符。而不会移除标签与文本之间的空白字符。看以下代码:
{% spaceless %}
<p>
<a href="foo/">
Foo
</a>
</p>
{% endspaceless %}
那么在渲染完成后,会变成以下的代码:
<p><a href="foo/">
Foo
</a></p>
autoescape
autoescape
标签:开启和关闭这个标签内元素的自动转义功能。
什么是自动转义:
自动转义(Autoescape) 是一种 保护机制,在模板中输出变量时,会把一些有特殊意义的符号转换成“安全”的形式,防止 HTML 或 JavaScript 被浏览器错误执行。
为什么要自动转义?为了防止用户提交恶意内容,让浏览器只显示内容、而不是执行代码。
假设你从数据库里取出这样一个字符串:
data = '<script>alert("你被黑了")</script>'
你在模板里这样写:
<p>{{ data }}</p>
如果自动转义是开启的(默认情况):Django 会自动把
<
,>
,"
等特殊字符“转义”成 HTML 安全形式:<p><script>alert("你被黑了")</script></p>
这样浏览器看到的只是普通文本,不会执行这段代码。
模板中默认是已经开启了自动转义功能
# 传递的上下文信息
context = {
"info":"<a href='www.baidu.com'>百度</a>"
}
# 模板中关闭自动转义
{% autoescape off %}
{{ info }}
{% endautoescape %}
{% autoescape on %}
{{ info }}
{% endautoescape %}
# 结果:
关闭自动转义功能:<a href='www.baidu.com'>百度</a>
开启自动转义功能:<a href='www.baidu.com'>百度</a>
verbatim 标签
{% verbatim %}
标签用于 在模板中写“原样的”代码片段,防止 Django 模板引擎去解析其中的 {{ }}
或 {% %}
。
也就是说,verbatim 块里面的内容 Django 会“当作普通字符串”,原封不动地输出,哪怕它看起来像模板语法。
为什么需要 verbatim?
举个例子:你在模板里写 Vue、React 或 JavaScript 的代码,像这种:
<div id="app"> {{ message }} </div>
Django 会误以为
{{ message }}
是模板变量,而不是 Vue 的语法,结果就报错或显示不对!这时候你就可以用
verbatim
标签包起来{% verbatim %} <div id="app"> {{ message }} </div> {% endverbatim %}
Django 渲染结果就是:
<div id="app"> {{ message }} </div>
完美保留了 Vue 的变量写法,不会被 Django 模板“误伤”。
使用场景:
场景 | 是否推荐用 verbatim |
---|---|
在模板中写 Vue.js | 非常适合 |
写 React 模板 | 非常适合 |
插入大量 JS 模板代码 | 推荐使用 |
普通 HTML 输出 | 不需要,Django 会自动处理 |
模版常用过滤器
在模版中,有时候需要对一些数据进行处理以后才能使用。一般在 Python 中我们是通过函数的形式来完成的。而在模版中,则是通过过滤器来实现的。过滤器使用的是 |
来使用。比如使用 add
过滤器,那么示例代码如下
{{ value|add:"2" }}
add
将传进来的参数添加到原来的值上面。这个过滤器会尝试将 值 和 参数 转换成整形然后进行相加。如果转换成整形过程中失败了,那么会将 值 和 参数 进行拼接。如果是字符串,那么会拼接成字符串,如果是列表,那么会拼接成一个列表。示例代码如下:
{{ value|add:"2" }}
如果 value 是等于4,那么结果将是6。
如果 value 是等于一个普通的字符串,比如 abc
,那么结果将是abc2
。
add 过滤器的源代码如下:
def add(value, arg):
"""Add the arg to the value."""
try:
return int(value) + int(arg)
except (ValueError, TypeError):
try:
return value + arg
except Exception:
return ''
cut
移除值中所有指定的字符串。类似于 python 中的 replace(args,"")
。示例代码如下:
{{ value|cut:" " }}
以上示例将会移除 value 中所有的空格字符。 cut 过滤器的源代码如下:
def cut(value, arg):
"""Remove all values of arg from the given string."""
safe = isinstance(value, SafeData)
value = value.replace(arg, '')
if safe and arg != ';':
return mark_safe(value)
return value
date
将一个日期按照指定的格式,格式化成字符串。示例代码如下:
# 数据
context = {
"birthday": datetime.now()
}
# 模版
{{ birthday|date:"Y/m/d" }}
那么将会输出 2025/02/01
。其中 Y
代表的是四位数字的年份, m
代表的是两位数字的月份, d
代表的是两位数字的日。
还有更多时间格式化的方式。见下表。
格式字符 | 描述 | 示例 |
---|---|---|
Y | 四位数字的年份 | 2018 |
m | 两位数字的月份 | 01-12 |
n | 月份,1-9前面没有0前缀 | 1-12 |
d | 两位数字的天 | 01-31 |
j | 天,但是1-9前面没有0前缀 | 1-31 |
g | 小时,12小时格式的,1-9前面没有0前缀 | 1-12 |
h | 小时,12小时格式的,1-9前面有0前缀 | 01-12 |
G | 小时,24小时格式的,1-9前面没有0前缀 | 1-23 |
H | 小时,24小时格式的,1-9前面有0前缀 | 01-23 |
i | 分钟,1-9前面有0前缀 | 00-59 |
s | 秒,1-9前面有0前缀 | 00-59 |
default
如果值被评估为 False
。比如 []
, ""
, None
, {}
等这些在 if
判断中为 False
的值,都会使用default
过滤器提供的默认值。示例代码如下:
{{ value|default:"nothing" }}
如果 value 是等于一个空的字符串。比如 ""
,那么以上代码将会输出 nothing
。
default_if_none
如果值是 None ,那么将会使用 default_if_none 提供的默认值。这个和 default 有区别, default 是所有被评估为 False
的都会使用默认值。而 default_if_none 则只有这个值是等于 None
的时候才会使用默认值。示例代码如下:
{{ value|default_if_none:"nothing" }}
如果 value 是等于 ""
也即空字符串,那么以上会输出空字符串。如果 value 是一个 None
值,以上代码才会输出 nothing 。
first
返回列表/元组/字符串中的第一个元素。示例代码如下:
{{ value|first }}
如果 value 是等于 [‘a’,’b’,’c’] ,那么输出将会是 a 。
last
返回列表/元组/字符串中的最后一个元素。示例代码如下
{{ value|last }}
如果 value 是等于 [‘a’,’b’,’c’] ,那么输出将会是 c 。
floatformat
使用四舍五入的方式格式化一个浮点类型。
如果这个过滤器没有传递任何参数。那么只会在小数点后保留一个小数,
如果小数后面全是0,那么只会保留整数。
当然也可以传递一个参数,标识具体要保留几个小数。
如果没有传递参数:
value | 模版代码 | 输出 |
---|---|---|
34.23234 | `{{ value | floatformat }} ` |
34.0000 | `{{ value | floatformat }} ` |
34.26000 | `{{ value | floatformat }} ` |
如果传递参数:
value | 模版代码 | 输出 |
---|---|---|
34.23234 | `{{ value | floatformat:3 }} ` |
34.0000 | `{{ value | floatformat:3 }} ` |
34.26000 | `{{ value | floatformat:3 }} ` |
join
类似与 Python 中的 join
,将列表/元组/字符串用指定的字符进行拼接。示例代码如下:
{{ value|join:"/" }}
如果 value 是等于 ['a','b','c']
,那么以上代码将输出 a/b/c
。
length
获取一个列表/元组/字符串/字典的长度。示例代码如下:
{{ value|length }}
如果 value 是等于 ['a','b','c']
,那么以上代码将输出 3
。
如果 value 为 None
,那么以上将返回0
。
lower
将值中所有的字符全部转换成小写。示例代码如下
{{ value|lower }}
如果 value 是等于 Hello World
。那么以上代码将输出hello world
upper
类似于 lower ,只不过是将指定的字符串全部转换成大写。
random
在被给的列表/字符串/元组中随机的选择一个值。示例代码如下:
{{ value|random }}
如果 value 是等于 ['a','b','c']
,那么以上代码会在列表中随机选择一个。
safe
标记一个字符串是安全的。也即会关掉这个字符串的自动转义。示例代码如下:
{{value|safe}}
如果 value 是一个不包含任何特殊字符的字符串,比如 <a>
这种,那么以上代码就会把字符串正常的输入。如果 value 是一串 html 代码,那么以上代码将会把这个 html 代码渲染到浏览器中。
# 如果value= <h1>Hello</h1>
{{value}} # 得到的结果: <h1>Hello</h1> ===> 在浏览器中显示的是字符串:<h1>Hello</h1>
{{value|safe}} # 得到的结果:<h1>Hello</h1> ===> 在浏览器中体现的是:一级标题 Hello
slice
类似于 Python 中的切片操作。示例代码如下:
{{ some_list|slice:"2:" }}
以上代码将会给 some_list 从 2 开始做切片操作。
如果 some_list=[1, 2, 3, 4, 5]
,那么切片得到的结果就是[3, 4, 5]
striptags
删除字符串中所有的 html 标签。示例代码如下:
{{ value|striptags }}
如果 value 是 <strong>hello world</strong>
,那么以上代码将会输出 hello world
。
truncatechars
如果给定的字符串长度超过了过滤器指定的长度。
那么就会进行切割,并且会拼接三个点来作为省略号。示例代码如下:
{{ value|truncatechars:5 }}
如果 value 是等于 北京欢迎您~
,那么输出的结果是 北京欢迎…
。
可能你会想,为什么不会 北京欢迎您…
呢。
因为三个点(…
)也占了一个字符,所以 北京欢迎
+…
的字符长度就是5
truncatechars_html
类似于 truncatechars ,只不过是不会切割 html 标签。示例代码如下:
{{ value|truncatechars_html:5 }}
如果 value 是等于 <p>北京欢迎您~</p>
,那么输出将是 <p>北京欢迎…</p>
。
模版结构
include模版
有时候一些代码是在许多模版中都用到的。
如果我们每次都重复的去拷贝代码那肯定不符合项目的规范。
一般我们可以把这些重复性的代码抽取出来,就类似于Python中的函数一样,以后想要使用这些代码的时候,就通过 include
包含进来。
# f-hotbook : 提取的代码
<div>
<h1>热门图书</h1>
<ul>
<li>book01</li>
<li>book02</li>
</ul>
</div>
# f-index:引入模版
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
# 在这里引入模版
{% include "f-hotbook.html" %}
</body>
</html>
# views.py
def template_form(request):
return render(request, 'f-index.html')
# path 函数
path("book/template", book_views.template_form, name="template_form"),
include
标签寻找路径的方式。也是跟 render
渲染模板的函数是一样的
默认 include
标签包含模版,会自动的使用主模版中的上下文,也即可以自动的使用主模版中的变量。
# f-hotbook : 提取的代码
<h1>热门图书: 使用主模版变量</h1>
<table>
<thead>
<tr>
<th>书籍</th>
<th>作者</th>
</tr>
</thead>
<tbody>
{% for book in books %}
<tr>
<td>{{book.name}}</td>
<td>{{book.author}}</td>
</tr>
{% endfor %}
</tbody>
</table>
# views.py : 传递 books变量
def template_form(request):
books = [
{"name": "三国演义", "author": "施耐庵"},
{"name": "活着", "author": "余华"},
{"name": "三体", "author": "刘慈欣"}
]
context = {
'books': books,
}
return render(request, 'f-index.html', context=context)
如果想传入一些其他的参数,那么可以使用 with
语句
# f-hotbook : 提取的代码
<h1>热门图书: with</h1>
{{ name }} -- {{ author }}
# f-index:引入模版
{% include "f-hotbook.html" with name="红楼梦" author="曹雪芹"%}
模板继承
在前端页面开发中。有些代码是需要重复使用的。这种情况可以使用 include
标签来实现。也可以使用另外一个比较强大的方式来实现,那就是模版继承。
模版继承类似于 Python 中的类,在父类中可以先定义好一些变量和方法,然后在子类中实现。模版继承也可以在父模版中先定义好一些子模版需要用到的代码,然后子模版直接继承就可以了。
并且因为子模版肯定有自己的不同代码,因此可以在父模版中定义一个block
接口,然后子模版再去实现。以下是父模版的代码:
# f-base.html: 父 模版
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}{% endblock %} -- 后缀名 </title>
{# 定义一些Js和CSS样式 #}
{% block js-css %}{% endblock %}
</head>
<body>
<header>
<UL>
<li><a href="/">父模板-首页</a></li>
<li><a href="f-hotbook.html"> 热门图书</a></li>
</UL>
</header>
{#abc 名字,叫什么都可以#}
{% block abc %}{% endblock %}
<footer> 表示页面的结尾部分,通常包含:版权信息 联系方式 链接跳转(隐私政策、条款等) 页脚导航</footer>
</body>
</html>
# f-extend: 子类模版继承
{% extends 'f-base.html' %}
{% block js-css %}
<style>
body{
background: pink;
}
</style>
{% endblock %}
{% block title %}
首页
{% endblock %}
{#输入 block + tap : 自动生成语法#}
{% block abc %}
{% include "f-hotbook.html" %}
{% endblock %}
需要注意的是:extends标签必须放在模版的第一行。子模板中的代码必须放在block中,否则将不会被渲染。
如果在某个 block
中需要使用父模版的内容,那么可以使用 {{block.super}}
来继承。比如上例,{%block title%}
,如果想要使用父模版的 title
,那么可以在子模版的 title block
中使用 {{ block.super }}
来实现。
# 如果我在f-base里面这么定义 title
<title>{% block title %}父类{% endblock %} </title>
# f-extend:如果我们希望使用 f-base的 ”父类“ 这个变量,那么需要 {{ block.super }} 来引入
{% block title %}
首页-{{ block.super }}
{% endblock %}
# 最后呈现的标题: 首页-父类
在定义 block
的时候,除了在 block
开始的地方定义这个 block
的名字,还可以在 block
结束的时候定义名字。比如 {% block title %}{% endblock title %}
。这在大型模版中显得尤其有用,能让你快速的看到 block 包含在哪里
加载静态文件
BASE_DIR
BASE_DIR
是 Django 项目里一个 表示项目根目录的路径变量,你通常会在 settings.py
文件最上面看到它的定义:
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
__file__
:当前这个 Python 文件的路径,也就是settings.py
的路径.resolve()
:把路径解析成绝对路径.parent
:返回这个路径的上一级目录.parent.parent
:再上一级,也就是项目的根目录
路径拼接的两种方式
os.path.join(BASE_DIR, "static")
这是传统的写法,用的是 Python 的标准库 os.path
os.path.join()
用于拼接路径,会根据操作系统自动使用正确的路径分隔符(/
或\
)BASE_DIR
是一个字符串或Path
对象(通常是字符串)"static"
是你要追加的文件夹名
BASE_DIR / "static"
这是更现代的写法,用的是 pathlib.Path
对象的语法(Python 3.4+)
BASE_DIR
是一个Path
类型的对象(不是字符串)- 使用
/
运算符拼接路径,更简洁、更优雅、更“面向对象” - 推荐在现代 Python(特别是 Django)中使用
对比
方面 | os.path.join() | Path / “xxx” |
---|---|---|
类型要求 | 字符串 | Path 对象 |
返回值 | 字符串 | Path 对象 |
可读性 | 较繁琐 | 简洁优雅 |
推荐使用 | 旧项目兼容 | 新项目推荐 |
如何加载静态资源
在一个网页中,不仅仅只有一个 html 骨架,还需要 css 样式文件, js 执行文件以及一些图片等。因此在 DTL 中加载静态文件是一个必须要解决的问题。在 DTL 中,使用 static
标签来加载静态文件。要使用 static
标签,首先需要 {% load static %}
。加载静态文件的步骤如下:
首先确保
django.contrib.staticfiles
已经添加到settings.INSTALLED_APPS
中。什么是
django.contrib.staticfiles
?这是 Django 内置的一个 用于处理静态文件的 app,包含:
- 自动寻找
static/
目录 - 自动帮你把静态文件复制到目标目录
- 开发模式下自动处理 URL 路由
所以:如果你在
INSTALLED_APPS
中添加了它,Django 就可以:- 识别项目和 app 里的
static/
目录 - 自动处理
/static/xxx.css
这样的请求
- 自动寻找
确保在 settings.py 中设置了 STATIC_URL 。比如:
STATIC_URL = 'static/'
核心作用是定义静态文件的 URL 前缀
当模板中引用静态文件(如
<link href="{% static 'css/style.css' %}"
)时,Django 会将{% static %}
标签替换为STATIC_URL
的值,生成完整的访问路径。<!-- 模板中的代码 --> <img src="{% static 'images/logo.png' %}"> <!-- 渲染后的 HTML --> <img src="/static/images/logo.png">
在已经安装了的
app
下创建一个文件夹叫做static
,然后再在这个static
文件夹下创建一个当前app
的名字的文件夹,再把静态文件放到这个文件夹下。例如你的app
叫做book
,有一个静态文件叫做HuoZhe.jpg
,那么路径为book/static/book/HuoZhe.jpg
。- 为什么在
app
下创建一个static
文件夹,还需要在这个static
下创建一个同app
名字的文件夹呢? - 原因是如果直接把静态文件放在
static
文件夹下,那么在模版加载静态文件的时候就是使用HuoZhe.jpg
,如果在多个app
之间有同名的静态文件,这时候可能就会产生混淆。而在static
文件夹下加了一个同名app
文件夹,在模版中加载的时候就是使用app/HuoZhe.jpg
,这样就可以避免产生混淆。
- 为什么在
如果有一些静态文件是不和任何
app
挂钩的。那么可以在settings.py
中添加STATICFILES_DIRS
,以后 DTL 就会在这个列表的路径中查找静态文件# 传统方式 STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static") ] # Django 方式 STATICFILES_DIRS = [ # 这个 static 可以变成 ABC 只是一个名称 BASE_DIR / "static" ]
在模版中使用 load 标签加载 static 标签。比如要加载在项目的 static 文件夹下的
style.css
的文件。# static.html {#加载静态资源#} {% load static %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>static</title> {# 引入CSS 样式 #} <link rel="stylesheet" href="{% static 'css/index.css' %}"> {# 引入JS 脚本 #} <script src="{% static 'js/index.js' %}"></script> </head> <body> <h1>静态文件加载</h1> </body> </html> # book.views.py def static_view(request): return render(request,"static.html") # urls.py path("static",book_views.static_view , name="static"),
如果不想每次在模版中加载静态文件都使用
load
加载static
标签,那么可以在settings.py
中的TEMPLATES/OPTIONS
添加'builtins':['django.templatetags.static']
,这样以后在模版中就可以直接使用static
标签,而不用手动的load
了。TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'templates'] , 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], # 加在这里 'builtins':['django.templatetags.static'] }, }, ]
如何处理用户上传的文件
怎么处理
我们假设用户已经把图片上传到
BASE_DIR/media
,那么我们要如何加载这个静态资源呢?配置
settings.py
MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media'
配置
urls.py
urlpatterns = [ path('admin/', admin.site.urls), ...... path("static", book_views.static_view, name="static"), ] # 这里的settings 就是指 settings.py 这个文件 from django.conf import settings from django.conf.urls.static import static # 仅开发阶段用 if settings.DEBUG: # 这段代码的意思: 如果用户访问 MEDIA_URL = '/media/' 这个路径 # 那么, 就会去 MEDIA_ROOT = BASE_DIR / 'media' 文件夹下寻找相关资源 urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
使用浏览器访问:
http://localhost:8000/media/001.png
。就会查看到media/001.png
的图片
问题1:为什么用户不能上传到static的文件夹
什么是 staticfiles?
Django 有一个 app:django.contrib.staticfiles
,它是用来管理前端用到的静态资源的,比如:
- JS 文件
- CSS 样式文件
- 图片(logo、背景图)
- 字体、icon 等
这些文件有几个特点:
特点 | 说明 |
---|---|
写死的 | 是你开发时写好的文件 |
不会变 | 发布上线之后不会动态生成或修改 |
只读的 | 项目运行的时候不会写入这些目录 |
可以缓存 | 可以通过浏览器或 CDN 缓存,加快访问速度 |
什么是 collectstatic?
你开发时,可能有多个 app,每个 app 里可能都有一个 static/
文件夹:
project/
├── app1/
│ └── static/app1/js/script.js
├── app2/
│ └── static/app2/css/style.css
当你上线项目时,Django 不会自己去找散落在各个 app
下的静态文件。所以你运行命令:
python manage.py collectstatic
它会把所有静态文件集中复制到一个地方(例如):
project/staticfiles/
然后你的服务器就只需要指向 staticfiles/
,就能提供所有静态资源。
那上传图片为啥不能放 static/
假设你这样做了:
- 用户上传了一张头像:
media/avatars/user1.jpg
- 你把这张图放到了
static/avatars/user1.jpg
那么你就要运行:
python manage.py collectstatic
才能把这张图片复制到 staticfiles/
目录,浏览器才能访问它。
问题来了:
- 每上传一张图片你都要
collectstatic
?用户自己操作你都没法自动触发! - 如果是 1000 个用户,每人上传头像,难道你手动收集 1000 次?
collectstatic
还可能清空以前的数据(根据配置),风险很大!
小结
你上传的图片 | 放在 static/ |
放在 media/ (推荐) |
---|---|---|
需要跑 collectstatic |
是 | 不需要 |
每次上传都要操作 | 是 | 不用管 |
被服务器识别 | 不会更新 | 立刻就能访问 |
推荐使用? | 不推荐 | 推荐 |
问题2:为什么只能在开发环境下这么弄
# 仅开发阶段用
if settings.DEBUG:
# 这段代码的意思: 如果用户访问 MEDIA_URL = '/media/' 这个路径
# 那么, 就会去 MEDIA_ROOT = BASE_DIR / 'media' 文件夹下寻找相关资源
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
因为 static()
是为开发环境准备的,生产环境下应该由 专业的 web 服务器(如 Nginx
) 来处理媒体文件!
static()
函数做了什么?
这个函数的作用是:让开发服务器(runserver
)也能响应图片/文件请求,
比如你访问:http://localhost:8000/media/avatars/cat.jpg
如果没有 static()
,这个请求不会被 Django 处理,返回 404。
但为什么生产环境不能这样做?
- Django 自带的
runserver
是为开发用的,不适合处理大量静态/媒体文件请求。 - 如果你用 Django 自带服务器处理图片:
- 效率低
- 无缓存策略
- 容易崩
- 正确做法是在部署时交给 专业 Web 服务器 来处理,比如:
内容 | 谁来负责 |
---|---|
Django 页面请求(视图函数) | Django |
图片、CSS、JS、媒体文件 | Nginx、Apache、CDN |
Mac环境下安装MySql8
安装网址:https://dev.mysql.com/downloads/mysql/
配置环境
安装后,在设置下面会有一个MySql的小图标:
这表明,mysql已经安装了,单击进入:
配置环境信息:
# 打开并编辑 zshrc 文件:
nano ~/.zshrc
# 在文件末尾添加这一行:
export PATH="/usr/local/mysql-8.0.12-macos10.13-x86_64/bin:$PATH"
# 让更改生效
source ~/.zshrc
忘记root用户密码
确保 mysql server 已经停止运行
查询mysql server 是否处于运行状态
sudo /usr/local/mysql/support-files/mysql.server status
如果正在运行,则将其停止
sudo /usr/local/mysql/support-files/mysql.server stop
以 skip-grant-tables
模式启动mysql: 也就是登录的时候可以不验证密码
sudo /usr/local/mysql/support-files/mysql.server start --skip-grant-tables
修改密码
登录 mysql
mysql -u root
设置密码为空
mysql> UPDATE mysql.user SET authentication_string=null WHERE User='root';
mysql> flush privileges;
mysql> exit;
再次登录 mysql
mysql -u root
更改密码
mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'yourpasswd';
mysql> flush privileges;
mysql> exit;
以正常模式启动 mysql
sudo /usr/local/mysql/support-files/mysql.server stop
sudo /usr/local/mysql/support-files/mysql.server start
至此,就可以正常登录 mysql
mysql -u root -p
为什么要先把密码置空,然后再修改
为什么不能直接设置密码:
UPDATE mysql.user SET authentication_string="password" WHERE User='root';
FLUSH PRIVILEGES;
在 MySQL 8 中会遇到几个问题,以下是原因和解释:
authentication_string
的使用方法在 MySQL 8 中,
authentication_string
存储的是已经加密的密码,而不仅仅是明文的密码。因此,像你写的"password"
这种明文密码是不合适的。MySQL 8 推荐使用
ALTER USER
语法来修改密码,而不是直接修改authentication_string
。直接修改会导致 密码加密方式不一致,从而导致无法正常使用该密码进行登录。
密码加密算法的影响
- MySQL 8 使用了更强的加密方式(如
caching_sha2_password
),而直接用UPDATE
修改authentication_string
可能导致加密方式没有自动更新,最终会导致 登录时使用不一致的密码验证插件,进而无法正确登录。
- MySQL 8 使用了更强的加密方式(如
正确的做法是: 1. 清空密码:先清空 root 用户的密码,确保能正常登录一次。 2. 修改密码:使用 ALTER USER
明确指定密码,并指定正确的加密插件(mysql_native_password
或 caching_sha2_password
)。
操作数据库
安装驱动
我们使用 Django 来操作 MySQL ,实际上底层还是通过 Python 来操作的。因此我们想要用 Django 来操作 MySQL ,首先还是需要安装一个驱动程序。在 Python3 中,驱动程序有多种选择。比如有 pymysql
以及 mysqlclient
等。这里我们就使用 pymysql
来操作。 pymysql
安装非常简单。只需要通过 pip install pymysql
或者 conda install pymysql
即可安装
常见 MySQL 驱动介绍:
MySQL-python
:也就是MySQLdb
。是对 C 语言操作 MySQL 数据库的一个简单封装。遵循了Python DB API v2
。但是只支持Python2
,目前还不支持Python3
。mysqlclient
:是MySQL-python
的另外一个分支。支持Python3
并且修复了一些 bug 。pymysql
:纯Python
实现的一个驱动。因为是纯 Python 编写的,因此执行效率不如MySQL-python
。并且也因为是纯 Python 编写的,因此可以和 Python 代码无缝衔接。MySQL Connector/Python
: MySQL 官方推出的使用纯 Python 连接 MySQL 的驱动。因为是纯 Python 开发的。效率不高
Django配置连接数据库
在操作数据库之前,首先先要连接数据库。这里我们以配置 MySQL 为例来讲解。 Django 连接数据库,不需要单独的创建一个连接对象。只需要在 settings.py 文件中做好数据库相关的配置就可以了。示例代码如下:
DATABASES = {
'default': {
# 数据库引擎(是mysql还是oracle等)
'ENGINE': 'django.db.backends.mysql',
# 数据库的名字
'NAME': 'demo',
# 连接mysql数据库的用户名
'USER': 'root',
# 连接mysql数据库的密码
'PASSWORD': 'root1234',
# mysql数据库的主机地址
'HOST': '127.0.0.1',
# mysql数据库的端口号
'PORT': '3306',
}
}
其中 engine 的选择还有以下:
django.db.backends.postgresql
django.db.backends.mysql
django.db.backends.sqlite3
django.db.backends.oracle
在使用pymysql遇到的问题
Error loading MySQLdb module
错误信息:
django.core.exceptions.ImproperlyConfigured: Error loading MySQLdb module.
Did you install mysqlclient?
说明你使用了 Django + MySQL 数据库,但缺少连接 MySQL 所需的 Python 库。
Django 默认使用 MySQLdb
(一个 C 扩展库)来连接 MySQL,而现代 Python 环境中推荐使用 mysqlclient
这个包,它是 MySQLdb
的一个兼容包装。
我目前使用的是Pymysql
,而且已经安装了 PyMySQL
,那你只需要告诉 Django 使用 PyMySQL
替代默认的 MySQLdb
,就可以解决遇到的错误了
文件结构:
Database_demo/
├── manage.py
├── Database_demo/
│ ├── __init__.py ← 在这个文件里加
│ ├── settings.py
│ └── ...
修改 Database_demo/__init__.py
,加上以下代码:
import pymysql
pymysql.install_as_MySQLdb()
mysqlclient 1.4.3 or newer is required
错误信息:
django.core.exceptions.ImproperlyConfigured: mysqlclient 1.4.3 or newer is required;
you have 1.0.2.
我安装的pymysql
版本:
(base) Database_demo % conda list | grep pymysql
pymysql 1.0.2 py312hecd8cb5_1
这个版本不是最新版本。因为是在conda的官方库下载的,更新会比较慢。
可以在第三方库里面下载最新版本
conda install -c conda-forge pymysql
(base) Database_demo % conda list | grep pymysql
pymysql 1.1.1 pyhd8ed1ab_1 conda-forge
使用原生 sql 语句操作
在 Django 中使用原生 sql 语句操作其实就是使用 python db api
的接口来操作。如果你的 mysql 驱动使用的是 pymysql
,那么你就是使用 pymysql
来操作的,只不过 Django 将数据库连接的这一部分封装好了,我们只要在 settings.py
中配置好了数据库连接信息后直接使用 Django 封装好的接口就可以操作了。示例代码如下:
from django.shortcuts import HttpResponse
# 使用django封装好的connection对象,会自动读取settings.py中数据库的配置信息
from django.db import connection
# Create your views here.
def index(request):
# 获取游标对象
cursor = connection.cursor()
# 拿到游标对象后执行sql语句
cursor.execute("select * from movie")
# 获取所有的数据
rows = cursor.fetchall()
# 遍历查询到的数据
for row in rows:
print(row)
return HttpResponse(rows)
以上的 execute
以及 fetchall
方法都是 Python DB API
规范中定义好的。任何使用 Python 来操作MySQL 的驱动程序都应该遵循这个规范。所以不管是使用 pymysql
或者是 mysqlclient
或者是mysqldb
,他们的接口都是一样的。
更多规范请参考:https://www.python.org/dev/peps/pep-0249/
Python DB API
下规范下cursor
对象常用接口
description
:如果cursor
执行了查询的 sql 代码。那么读取cursor.description
属性的时候,将返回一个列表,这个列表中装的是元组,元组中装的分别是(name,type_code,display_size,internal_size,precision,scale,null_ok)
,其中name
代表的是查找出来的数据的字段名称,其他参数暂时用处不大。rowcount
:代表的是在执行了 sql 语句后受影响的行数。close
:关闭游标。关闭游标以后就再也不能使用了,否则会抛出异常execute(sql,[parameters])
:执行某个 sql 语句。如果在执行 sql 语句的时候还需要传递参数,那么可以传给parameters
参数。示例代码如下:
# %s 是参数占位符,防止 SQL 注入。
cursor.execute("select * from movie where id = %s and name like %s ", [1,"%活着%"])
fetchone
:在执行了查询操作以后,获取第一条数据。fetchmany(size)
:在执行查询操作以后,获取多条数据。具体是多少条要看传的size
参数。如果不传size
参数,那么默认是获取第一条数据。fetchall
:获取所有满足 sql 语句的数据。例子:
rom django.shortcuts import HttpResponse # 使用django封装好的connection对象,会自动读取settings.py中数据库的配置信息 from django.db import connection # Create your views here. from django.shortcuts import HttpResponse # 使用django封装好的connection对象,会自动读取settings.py中数据库的配置信息 from django.db import connection ''' fetch 之后的电影就不会还回去了 ''' def index(request): # 获取游标对象 cursor = connection.cursor() # 拿到游标对象后执行sql语句 # %s 是参数占位符,防止 SQL 注入。 #cursor.execute("select * from movie where id = %s and name like %s ", [1,"%活着%"]) cursor.execute("select * from movie ") # 输出 :(('id', 3, None, 11, 11, 0, True), ('name', 254, None, 300, 300, 0, True)) print(cursor.description) # 输出 6 print(cursor.rowcount) # 获取 1 调数据 # (1, '活着') print(cursor.fetchone()) # 获取任意多的数据 # ((2, '霸王别姬'), (3, '花样年华')) print(cursor.fetchmany(2)) # 获取所有的数据 # (4, '芙蓉镇')(5, '警察故事')(6, '大话西游') rows = cursor.fetchall() # 遍历查询到的数据 for row in rows: print(row) # 关闭游标 cursor.close() return HttpResponse(rows)
打印SQL
配置LOGGING
修改 settings.py
:
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django.db.backends': {
# 关键点:level=DEBUG 打印SQL日志,level=WARNING 就不好打印SQL日志
'level': 'WARNING',
'handlers': ['console'],
},
}
}
QuerySet 的 query 属性
QuerySet.query
是 Django ORM 提供的一个接口,用来查看 Django 生成的 SQL 查询语句(SELECT)
@property
def query(self):
if self._deferred_filter:
negate, args, kwargs = self._deferred_filter
self._filter_or_exclude_inplace(negate, args, kwargs)
self._deferred_filter = None
return self._query
QuerySet.query
返回的是一个django.db.models.sql.query.Query
对象它的
__str__()
方法会将内部结构转成 SQL 字符串你通常只在调试或SQL 优化时使用它
query
属性只能用于 读取类操作(SELECT
),它不能用于create()
、update()
、delete()
这类 写操作(INSERT
、UPDATE
、DELETE
)。因为.query
属于 QuerySet 对象,而写操作通常是通过 方法调用 实现的,它们不会返回 QuerySet,而是执行 SQL 并返回结果(如影响行数或对象本身)。
举个例子:
qs = Book.objects.filter(price__gt=100)
print(qs.query)
输出的会是类似这样的 SQL:
SELECT "app_book"."id", "app_book"."title", "app_book"."price"
FROM "app_book"
WHERE "app_book"."price" > 100
connection.queries
connection.queries
是什么? connection.queries
是 Django 中的一个列表,记录了当前请求周期中执行的所有 SQL 语句。每一项是一个字典,包含:
{
'sql': 'SELECT AVG("app_book"."price") AS "price__avg" FROM "app_book"',
'time': '0.002'
}
什么叫当前请求周期?
在 Django 中,每当用户通过浏览器访问你的网站,比如打开某个页面,Django 就会:
- 接收这个请求(Request)
- 交给对应的视图函数(View)处理
- 执行相关的数据库操作 / 业务逻辑
- 返回响应(Response)给用户
这个完整的过程,就叫一次 请求-响应周期(request-response cycle),简称:“当前请求周期” = 当前这个用户访问页面的一次完整过程
注意事项:
connection.queries
只有在DEBUG=True
时可用(开发环境默认开启)。在生产环境下该方法可能被禁用以提高性能。
查询最近一次的SQL:
from django.db import connection
from django.db.models import Avg
from django.http import HttpResponse
from .models import Book
def index(request):
# 清空之前记录的 SQL 查询(可选)
connection.queries.clear()
# 执行聚合查询
result = Book.objects.aggregate(Avg('price'))
# 获取最后一条 SQL 语句(即本次查询)
sql = connection.queries[-1]['sql']
print(sql) # 打印 SQL
print(result) # 打印结果字典,例如 {'price__avg': 25.00}
return HttpResponse("index success")
打印所有执行过的 SQL
from django.db import connection
from django.db.models import Avg
from django.http import HttpResponse
from .models import Book
def index(request):
connection.queries.clear() # 可选:清空之前的 SQL 记录
# 执行多个查询
result = Book.objects.aggregate(Avg('price'))
books = Book.objects.filter(price__gt=20).order_by('title')
exists = Book.objects.exists()
# 打印所有执行过的 SQL
print("\n=== SQL 查询列表 ===")
for q in connection.queries:
print(q['sql'])
return HttpResponse("index success")
ORM模型
ORM模型介绍
随着项目越来越大,采用写原生SQL的方式在代码中会出现大量的SQL语句,那么问题就出现了:
SQL语句重复利用率不高,越复杂的SQL语句条件越多,代码越长。会出现很多相近的SQL语句。
很多SQL语句是在业务逻辑中拼出来的,如果有数据库需要更改,就要去修改这些逻辑,这会很容易漏掉对某些SQL语句的修改。
写SQL时容易忽略web安全问题,给未来造成隐患。SQL注入。
ORM
,全称 Object Relational Mapping
,中文叫做对象关系映射,通过 ORM
我们可以通过类的方式去操作数据库,而不用再写原生的SQL
语句。通过把表映射成类,把行作实例,把字段作为属性, ORM
在执行对象操作的时候最终还是会把对应的操作转换为数据库原生语句。使用 ORM
有许多优点:
- 易用性:使用 ORM 做数据库的开发可以有效的减少重复SQL语句的概率,写出来的模型也更加直观、清晰。
- 性能损耗小: ORM 转换成底层数据库操作指令确实会有一些开销。但从实际的情况来看,这种性能损耗很少(不足5%),只要不是对性能有严苛的要求,综合考虑开发效率、代码的阅读性,带来的好处要远远大于性能损耗,而且项目越大作用越明显。
- 设计灵活:可以轻松的写出复杂的查询。
- 可移植性: Django 封装了底层的数据库实现,支持多个关系数据库引擎,包括流行的 MySQL 、PostgreSQL 和 SQLite 。可以非常轻松的切换数据库
创建ORM模型
ORM
模型一般都是放在 app
的 models.py
文件中。每个 app
都可以拥有自己的模型。并且如果这个模型想要映射到数据库中,那么这个 app
必须要放在 settings.py
的 INSTALLED_APP
中进行安装。以下是写一个简单的书籍 ORM
模型。示例代码如下:
from django.db import models
'''
Python 的继承语法
class 子类名(父类名):
# 子类的定义
models.Model:
这是 Django 的 模型基类。你定义的 Book 是一个数据库模型(就是表),
它必须继承自 models.Model,才能拥有 Django 提供的那些 ORM 功能,比如:
数据表自动生成
字段自动映射
.save()、.filter()、.get() 等数据库操作方法
'''
class Book(models.Model):
# CharField:用于存储较短的字符串(比如书名、用户名等)。
# max_length=20:字符串最大长度为 20。
# null=False:数据库中不能为空(NOT NULL)。
name = models.CharField(max_length=20,null=False)
author = models.CharField(max_length=20,null=False)
# DateTimeField:存储日期和时间。
# auto_now_add=True:在创建对象时,自动写入当前时间(只设置一次)。
pub_time = models.DateTimeField(auto_now_add=True)
# FloatField:表示价格,浮点数。
# default=0:如果没有传值,默认价格为 0。
price = models.FloatField(default=0)
def __str__(self):
return f"{self.id},{self.name},{self.author},{self.pub_time.strftime('%Y-%m-%d')},{self.price:.2f}"
还有一个字段我们没有写,就是主键 id
,在 django
中,如果一个模型没有定义主键,那么将会自动生成一个自动增长的 int
类型的主键,并且这个主键的名字就叫做 id
。
映射模型到数据库中
将 ORM 模型映射到数据库中,总结起来就是以下几步:
在
settings.py
中,配置好DATABASES
,做好数据库相关的配置。在
app
中的models.py
中定义好模型,这个模型必须继承自django.db.models
。将这个
app
添加到settings.py
的INSTALLED_APP
中。INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'movie.apps.MovieConfig', 'book.apps.BookConfig', ]
在命令行终端,进入到项目所在的路径,然后执行命令
python manage.py makemigrations
来生成迁移脚本文件。(base) xxxxxxx@xxxxxxxMacBook-Pro Database_demo % python manage.py makemigrations Migrations for 'book': book/migrations/0001_initial.py + Create model Book
同样在命令行中,执行命令
python manage.py migrate
来将迁移脚本文件映射到数据库中。(base) xxxx@xxxxxMacBook-Pro Database_demo % python manage.py migrate Operations to perform: Apply all migrations: admin, auth, book, contenttypes, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... 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 book.0001_initial... OK Applying sessions.0001_initial... OK
由于Django框架自带了一些App,在
INSTALLED_APP
里面可以看到。所有在第一次migrate
的时候,也会把系统自带的一些模型也进行创建。
基本CRUD操作
添加
def add(request):
book = Book(name="三国演义", author="罗贯中", price=100)
book.save()
book = Book(name="红楼梦", author="曹雪芹", price=200)
book.save()
book = Book(name="活着", author="余华", price=50)
book.save()
return HttpResponse("新增成功")
查询
def select(request):
# 查询所有
books = Book.objects.all()
# <QuerySet [<Book: 1,三国演义,罗贯中,2025-04-15,100.00>,
# <Book: 2,红楼梦,曹雪芹,2025-04-15,200.00>,
# <Book: 3,活着,余华,2025-04-15,50.00>]>
print(books)
# 数据过滤: 条件查询
filters = Book.objects.filter(price=100,name="三国演义")
# <QuerySet [<Book: 1,三国演义,罗贯中,2025-04-15,100.00>]>
print(filters)
# 获得单个对象
one = Book.objects.get(name="活着")
# 3,活着,余华,2025-04-15,50.00
print(one)
# 排序
## 按照 pub_time 升序
#<QuerySet [<Book: 1,三国演义,罗贯中,2025-04-15,100.00>,
# <Book: 2,红楼梦,曹雪芹,2025-04-15,200.00>,
# <Book: 3,活着,余华,2025-04-15,50.00>]>
books2 = Book.objects.order_by("pub_time")
print(books2)
## 按照 pub_time 降序
# <QuerySet [<Book: 3,活着,余华,2025-04-15,50.00>,
# <Book: 2,红楼梦,曹雪芹,2025-04-15,200.00>,
# <Book: 1,三国演义,罗贯中,2025-04-15,100.00>]>
books3 = Book.objects.order_by("-pub_time")
print(books3)
return HttpResponse("查询成功")
修改
def update(request):
book = Book.objects.get(id=1)
book.price += 10
book.save()
return HttpResponse("更改成功")
删除
def deletes(request):
book = Book.objects.get(id=1)
book.delete()
return HttpResponse("删除成功")
字段Field及其参数
常用字段
AutoField
AutoField
:映射到数据库中是 int
类型,可以有自动增长的特性。一般不需要使用这个类型,如果不指定主键,那么模型会自动的生成一个叫做 id
的自动增长的主键。如果你想指定一个其他名字的并且具有自动增长的主键,使用 AutoField
也是可以的。范围约为 -2^31 ~ 2^31-1
(约 ±21 亿)。
id = models.AutoField(primary_key=True)
BigAutoField
BigAutoField
: 64位的整形,类似于 AutoField
,相当于数据库中的 BIGINT AUTO_INCREMENT
。范围约为 -2^63 ~ 2^63-1
(约 ±9 亿亿)。
id = models.BigAutoField(primary_key=True)
在Django框架中,默认自动生成的主键ID就是BigAutoField
。因为在项目的setting.py
文件下面配置了:
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
如何把上面的配置删了,那么默认的主键类型就是AutoField
UUIDField
只能存储 uuid 格式的字符串。 uuid 是一个32位的全球唯一的字符串,一般用来作为主键。
BooleanField
在模型层面接收的是 True
/False
。在数据库层面是 tinyint
类型。如果没有指定默认值,默认值是None
。
is_published = models.BooleanField(default=False)
CharField
在数据库层面是 varchar
类型。在 Python 层面就是普通的字符串。这个类型在使用的时候必须要指定最大的长度,也即必须要传递 max_length
这个关键字参数进去。
name = models.CharField(max_length=100)
TextField
大量的文本类型。映射到数据库中是longtext类型。
用途:大段文本,比如文章正文、评论内容等
content = models.TextField()
EmailField
类似于 CharField
。在数据库底层也是一个 varchar
类型。最大长度是254个字符
URLField
类似于 CharField ,只不过只能用来存储 url 格式的字符串。并且默认的 max_length 是200。
FileField
用来存储文件的。这个请参考后面的文件上传章节部分。
ImageField
用来存储图片文件的。这个请参考后面的图片上传章节部分。
DateField
日期类型。在 Python 中是 datetime.date
类型,可以记录年月日。在映射到数据库中也是 date
类型。使用这个 Field 可以传递以下几个参数:
auto_now
:在每次这个数据保存的时候,都使用当前的时间。比如作为一个记录修改日期的字段,可以将这个属性设置为 True 。auto_now_add
:在每次数据第一次被添加进去的时候,都使用当前的时间。比如作为一个记录第一次入库的字段,可以将这个属性设置为True
。
pub_date = models.DateField(auto_now_add=True)
DateTimeField
日期时间类型,类似于 DateField
。不仅仅可以存储日期,还可以存储时间。映射到数据库中是datetime
类型。这个 Field 也可以使用 auto_now
和 auto_now_add
两个属性
updated = models.DateTimeField(auto_now=True)
TimeField
时间类型。在数据库中是 time
类型。在 Python 中是 datetime.time
类型。
FloatField
浮点类型。映射到数据库中是 float 类型。
price = models.FloatField(default=0.0)
IntegerField
整形。值的区间是 -2147483648——2147483647 。
age = models.IntegerField(default=0)
BigIntegerField
大整形。值的区间是 -9223372036854775808——9223372036854775807
PositiveIntegerField
正整形。值的区间是 0——2147483647
SmallIntegerField
小整形。值的区间是 -32768——32767
PositiveSmallIntegerField
正小整形。值的区间是 0——32767
Field的常用参数
更多 Field 参数请参考官方文档:https://docs.djangoproject.com/zh-hans/5.0/ref/models/fields/
null 和 blank
null
: 表示是否允许这个字段在数据库中存储 NULL
值。
null=True
: 表示 数据库中这个字段可以为 NULL
默认为 False
影响的是数据库中的字段约束。
name = models.CharField(max_length=50, null=True)
blank
: 是否允许这个字段在表单验证时为空(不填写)。
blank=True
: 在表单中这个字段可以留空。
默认为 False
。
影响的是 Django 表单(包括后台管理系统 admin、ModelForm、验证机制等)。
name = models.CharField(max_length=50, blank=True)
null
vs blank
的区别总结:
参数 | 作用层面 | 影响 | 默认值 |
---|---|---|---|
null |
数据库 | 是否存储 NULL |
False |
blank |
表单(验证) | 是否可留空 | False |
在使用字符串相关的Field (CharField
/TextField
)的时候,官方推荐尽量不要使用这个参数,也就是保持默认值 False
。因为 Django
在处理字符串相关的 Field 的时候,如果这个 Field 的 null=False
,即使你没有给这个Field 传递任何值,那么 Django 也会使用一个空的字符串 ""
来作为默认值存储进去。因此如果再使用null=True
, Django 会产生两种空值的情形(NULL
或者空字符串)。如果想要在表单验证的时候允许这个字符串为空,那么建议使用 blank=True
。
def add(request):
# 这里我没有给author赋值
book = Book(name="巴黎圣母院", price=100)
book.save()
return HttpResponse("新增成功")
但是在数据库里面,存了空字符串:
db_column
这个字段在数据库中的名字。如果没有设置这个参数,那么将会使用模型中属性的名字。
class Book(models.Model):
name = models.CharField(max_length=100, db_column='book_name')
- Django 中使用的是字段名
name
。如果不设置db_column='book_name'
这个参数,那么在数据库的列名也会是name
- 但数据库表中该字段的列名是
book_name
。
default
默认值。可以为一个值,或者是一个函数,但是不支持 lambda 表达式。并且不支持列表/字典/集合等可变的数据结构。
primary_key
是否为主键。默认是 False 。
unique
在表中这个字段的值是否唯一。一般是设置手机号码/邮箱等。
模型中Meta配置
对于一些模型级别的配置。我们可以在模型中定义一个类,叫做 Meta
。然后在这个类中添加一些类属性来控制模型的作用。
模型:
class Book(models.Model):
name = models.CharField(max_length=20, null=False)
author = models.CharField(max_length=20, null=False)
pub_time = models.DateTimeField(auto_now_add=True)
price = models.FloatField(default=0)
class Meta:
# 指定这个模型对应的数据库中的表名为 book_model。
# 如果不指定,Django 默认使用 app名_模型名 作为表名,比如 book_book
db_table = 'book_model'
# 这是指定查询结果的默认排序规则。
# 首先:按 name 字段升序排序。
# 接着:按 pub_time 字段降序排序(- 号表示降序)
ordering = ['name', '-pub_time']
def __str__(self):
return f"{self.id},{self.name},{self.author},{self.pub_time.strftime('%Y-%m-%d')},{self.price:.2f}"
代码:
def book_order(request):
# 伪代码:SELECT * FROM book_model ORDER BY name ASC, pub_time DESC;
books = Book.objects.all()
'''
<QuerySet [<Book: 4,巴黎圣母院,,2025-04-15,100.00>,
<Book: 3,活着,余华,2025-04-15,50.00>,
<Book: 2,红楼梦,曹雪芹,2025-04-15,200.00>]>
'''
print(books)
# 在查询时手动覆盖它, 按照价格升序
books2 = Book.objects.order_by("price")
'''
<QuerySet [<Book: 3,活着,余华,2025-04-15,50.00>,
<Book: 4,巴黎圣母院,,2025-04-15,100.00>,
<Book: 2,红楼梦,曹雪芹,2025-04-15,200.00>]>
'''
print(books2)
return HttpResponse("排序成功")
改了模型,记得要重新迁移:
python manage.py makemigrations
python manage.py migrate
外键
外键的介绍
在 MySQL 中,表有两种引擎,一种是 InnoDB
,另外一种是 myisam
。如果使用的是 InnoDB
引擎,是支持外键约束的。外键的存在使得 ORM 框架在处理表关系的时候异常的强大。因此这里我们首先来介绍下外键在 Django 中的使用。
类定义为 class ForeignKey(to,on_delete,**options)
。
to
:外键指向的模型on_delete
: 当被关联对象被删除时,当前模型中该外键字段应当如何处理?值 说明 CASCADE
级联删除(外键对象被删,这条记录也会被删) PROTECT
拒绝删除(抛出 ProtectedError) SET_NULL
设置为 NULL(需要设置 null=True) SET_DEFAULT
设置为默认值(需要 default=…) SET(...)
设置为某个值或函数返回值 DO_NOTHING
什么都不做(容易导致数据库报错) 常用可选参数(
**options
)参数 说明 null=True
数据库中可以为 NULL blank=True
表单中可以为空 related_name='books'
反向查询用,比如 author.books.all()
db_column='author_id'
数据库中字段名 verbose_name='作者'
人类可读名称 default=...
默认值
from django.db import models
# 有一个 User 和一个 Article 两个模型。
# 一个 User 可以发表多篇文章,一个 Article 只能有一个 Author
class User(models.Model):
username = models.CharField(max_length=20)
password = models.CharField(max_length=100)
class Article(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
# 每一个article 都会有一个作者,所有article要分配一个user
# CASCADE: 外键对象被删除,这条记录也会被删除
# 如果user1被删除,user1相关的article也会被删除
author = models.ForeignKey("User", on_delete=models.CASCADE)
外键对象的添加操作:
def add_article(request):
author = User(username='张三', password='111111')
# 要先保存这个:Django 不允许你保存一个还没保存到数据库的外键对象
author.save()
article = Article(title='abc', content='123')
article.author = author
article.save()
return HttpResponse("add successfully")
在 article 的实例中可以通过 author 属性来操作对应的 User 模型。这样使用起来非常的方便。示例代码如下:
def update_user_by_article(request):
article = Article.objects.get(id=1)
article.author.username = "李四"
# 只保存了 article,而 author 是一个外键关联的对象,
# article.save() 并不会影响 author 的字段。
# article.save()
article.author.save()
user = User.objects.get(id=1)
print(user.username) # 李四 # 成功修改成李四
return HttpResponse("update successfully")
为什么使用了 ForeignKey
后,就能通过 author 访问到对应的 user 对象呢。因此在底层, Django 为Article 表添加了一个 属性名_id
的字段(比如author的字段名称是author_id
),这个字段是一个外键,记录着对应的作者(User)的主键。以后通过 article.author
访问的时候,实际上是先通过 author_id
找到对应的数据,然后再提取 User 表中的这条数据,形成一个模型。
如果想要引用另外一个 app 的模型,那么应该在传递 to 参数
的时候,使用 app.model_name
进行指定。以上例为例,如果 User 和 Article 不是在同一个 app 中,那么在引用的时候的示例代码如下:
from django.db import models
# User模型在user这个app中
class User(models.Model):
username = models.CharField(max_length=20)
password = models.CharField(max_length=100)
# Article模型在article这个app中
class Article(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
# 使用 `app.model_name` 进行指定
author = models.ForeignKey("user.User", on_delete=models.CASCADE)
如果模型的外键引用的是本身自己这个模型,那么 to 参
数可以为 'self'
,或者是这个模型的名字。
在论坛开发中,一般评论都可以进行二级评论,即可以针对另外一个评论进行评论,那么在定义模型的时候就需要使用外键来引用自身。
示例代码如下:
class Comment(models.Model):
content = models.TextField()
origin_comment = models.ForeignKey('self', on_delete=models.CASCADE, null=True)
# origin_comment = models.ForeignKey('Comment', on_delete=models.CASCADE, null=True)
外键字段在数据库中的命名规则
默认规则
数据库列名 = 外键字段名 + _id
author = models.ForeignKey("User", on_delete=models.CASCADE)
- 数据库中生成的列名:
author_id
(注意不是author
!) - Django 自动处理:
- 在模型层面,你仍然通过
article.author
访问(Django 隐藏了_id
后缀)。 - 在数据库层面,实际存储的是
author_id
(即User
表的主键值)。
- 在模型层面,你仍然通过
自定义列名
如果想手动指定数据库列名,可以使用 db_column
参数:
author = models.ForeignKey("User", on_delete=models.CASCADE, db_column="user_id")
- 数据库中生成的列名:
user_id
(覆盖默认的author_id
) - 模型访问方式不变:仍用
article.author
。
外键删除操作
on_delete
: 当被关联对象被删除时,当前模型中该外键字段应当如何处理?
值 | 说明 |
---|---|
CASCADE |
级联删除(外键对象被删,这条记录也会被删) |
PROTECT |
拒绝删除(抛出 ProtectedError) |
SET_NULL |
设置为 NULL(需要设置 null=True) |
SET_DEFAULT |
设置为默认值(需要 default=…) |
SET(...) |
设置为某个值或函数返回值 |
DO_NOTHING |
什么都不做(容易导致数据库报错) |
如果一个模型 A 拥有一个外键 ForeignKey
指向模型 B,并且设置了 on_delete=PROTECT
,当你尝试删除 B 中某个对象时,如果这个对象正在被 A 的某条记录引用,那么会抛出一个 ProtectedError
异常,阻止删除操作。
必须先删除引用它的 A(即拥有外键的那一方),才能删除被引用的 B。
# 假设设置为PROTECT
class Article(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
author = models.ForeignKey("User", on_delete=models.PROTECT)
# 如果直接这么删除,会报ProtectedError
'''
django.db.models.deletion.ProtectedError:
("Cannot delete some instances of model 'User'
because they are referenced through protected foreign keys:
'Article.author'.", {<Article: Article object (1)>})
'''
def delete_user(request):
user = User.objects.get(id=1)
user.delete()
return HttpResponse("delete successfully")
# 所以需要先删除Article 才能删除 user
def delete_user(request):
# user = User.objects.get(id=1)
# user.delete()
Article.objects.filter(author_id=1).delete()
User.objects.get(id=1).delete()
return HttpResponse("delete successfully")
外键赋值操作
class BlogComment(models.Model):
content = models.TextField(verbose_name="内容")
pub_time = models.DateTimeField(auto_now_add=True, verbose_name="发布时间")
blog = models.ForeignKey(
Blog,
on_delete=models.CASCADE,
# related_name="comments":设置这个属性就可以通过 Blog.comments查到这个blog下面的所有评论
related_name="comments",
verbose_name="所属博客",
)
User = get_user_model()
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="作者")
@require_POST
@login_required()
def pub_comment(request):
blog_id = request.POST.get("blog_id")
content = request.POST.get("content")
# 一个外键赋值的是字符串或者数字blog_id=blog_id;另一个赋值的是对象author=request.user
BlogComment.objects.create(content=content, blog_id=blog_id, author=request.user)
# 重新加载博客详情页
return redirect(reverse("blog:blog_detail", kwargs={"blog_id": blog_id}))
直接赋外键对象(author=request.user
)
- 本质:传入的是一个完整的
User
模型实例(对象)。 - Django 内部处理:
- 自动提取该对象的 主键值(
user.id
) 存入数据库的author_id
列。 - 无需手动处理 ID,ORM 隐式完成转换。
- 自动提取该对象的 主键值(
赋 ID 值(blog_id=blog_id
)
- 本质:直接传入外键关联的主键值(如
blog_id=3
)。 - Django 内部处理:
- 直接将值存入数据库的
blog_id
列。 - 需要调用者确保该 ID 在关联表(
Blog
)中存在,否则可能引发外键约束错误。
- 直接将值存入数据库的
哪种方式更安全
赋对象更安全:Django 会自动验证对象是否存在。
赋 ID 需手动验证:需确保 ID 有效,否则可能报错
try: Blog.objects.get(id=blog_id) # 先验证存在性 BlogComment.objects.create(blog_id=blog_id, ...) except Blog.DoesNotExist: raise ValueError("Invalid blog_id")
效率差异
- 赋对象:若对象尚未从数据库加载(如
request.user
已通过中间件加载,无额外开销)。 - 赋 ID:绝对高效(但需权衡代码可读性)。
外键的”双刃剑”特性
优势面(为什么我们需要外键)
# 1. 极简的关联查询
article.author.name # 直接获取作者姓名
# 2. 自动数据完整性保护
on_delete=models.PROTECT # 防止误删关联数据
# 3. 内置的预取优化
Article.objects.select_related('author') # 单查询解决N+1问题
# 4. 自动生成后台关系界面
# (Django Admin自动生成关联操作界面)
劣势面(为什么需要谨慎使用)
# 1. 写入性能下降测试对比
# 无外键表:每秒插入10,000条
# 有外键表:每秒插入约6,000条(下降40%)
# 2. 级联删除风险
on_delete=models.CASCADE # 可能意外删除大量关联数据
# 3. 迁移复杂度增加
# 外键变更需要更复杂的迁移操作
Django 中不建议使用外键的场景
- 高频写入的表(如日志、埋点数据)
- 外键的约束检查和索引维护会显著降低写入速度。
- 超大规模数据表(千万级以上的数据)
- 外键的 JOIN 查询可能成为性能瓶颈,改用
IntegerField
+ 手动查询更可控。
- 外键的 JOIN 查询可能成为性能瓶颈,改用
- 需要极致写入性能的场景(如实时交易系统)
- 外键的锁机制可能影响并发吞吐量,可考虑逻辑外键(
user_id
代替ForeignKey
)。
- 外键的锁机制可能影响并发吞吐量,可考虑逻辑外键(
- 历史数据/归档数据
- 数据不再变更,外键约束无意义,反而增加存储和查询开销。
- 微服务/分布式架构
- 跨服务数据关联不适合用数据库外键,应通过 API 或事件驱动维护一致性。
- 需要灵活的数据关系(如多租户动态关联)
- 外键的静态模型限制灵活性,改用
GenericForeignKey
或 JSON 字段。
- 外键的静态模型限制灵活性,改用
- 迁移成本敏感的项目
- 外键变更可能导致复杂的迁移操作,无外键更易调整表结构。
外键对数据库查询效率的影响分析
外键对查询的正面影响
(1) 自动创建索引,加速查询
# Django 自动在外键字段上创建索引
class Order(models.Model):
customer = models.ForeignKey(User, on_delete=models.CASCADE)
# 相当于数据库执行: CREATE INDEX idx_order_customer_id ON order(customer_id)
- 优点:基于外键的等值查询(如
Order.objects.filter(customer_id=1)
)会非常高效。
(2) 简化复杂关联查询
# 无需手动JOIN,Django ORM自动处理关联
orders = Order.objects.select_related('customer').filter(customer__name='Alice')
- 优点:比手动写多表 JOIN 更易维护。
外键对查询的负面影响
(1) JOIN 操作可能成为性能瓶颈
# 多级外键嵌套时,生成的SQL可能很复杂
Book.objects.select_related('author__country').filter(author__country__name='China')
- 问题:
- 每增加一级关联,查询复杂度指数级上升
- 大表 JOIN 可能导致全表扫描(即使有索引)
(2) 索引维护成本
- 写入开销:每次插入/更新外键字段时,数据库需要更新索引结构。
- 存储开销:外键索引可能占用大量空间(尤其是复合索引)。
(3) 子查询陷阱
# 反向查询时可能生成低效SQL
users = User.objects.filter(article__title='Django')
# 实际SQL可能是: SELECT ... FROM user WHERE id IN (SELECT author_id FROM article WHERE title='Django')
- 问题:
- 某些数据库(如 MySQL)对
IN + 子查询
优化较差 - 应改用
exists()
或JOIN
优化。
- 某些数据库(如 MySQL)对
表关系
一对多
应用场景:比如文章和作者之间的关系。一个文章只能由一个作者编写,但是一个作者可以写多篇文章。文章和作者之间的关系就是典型的多对一的关系。
实现方式:一对多或者多对一,都是通过 ForeignKey 来实现的。还是以文章和作者的案例进行讲解。
from django.db import models
class User(models.Model):
username = models.CharField(max_length=20)
password = models.CharField(max_length=100)
class Article(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
author = models.ForeignKey("User", on_delete=models.CASCADE)
那么以后在给 Article 对象指定 author ,就可以使用以下代码来完成:
author = User(username='zhiliao',password='111111')
# 要先保存到数据库中
author.save()
article = Article(title='abc',content='123')
article.author = author
article.save()
并且以后如果想要获取某个用户下所有的文章,可以通过 article_set
来实现。示例代码如下
def get_article(request):
user = User.objects.get(id=4)
articles=user.article_set.all()
for article in articles:
print(article.title)
return HttpResponse("get successfully")
一对一
应用场景:比如一个用户表和一个用户信息表。在实际网站中,可能需要保存用户的许多信息,但是有些信息是不经常用的。如果把所有信息都存放到一张表中可能会影响查询效率,因此可以把用户的一些不常用的信息存放到另外一张表中我们叫做 UserExtension
。但是用户表 User
和用户信息表 UserExtension
就是典型的一对一了。
实现方式: Django 为一对一提供了一个专门的 Field 叫做 OneToOneField
来实现一对一操作。示例代码如下:
class User(models.Model):
username = models.CharField(max_length=20)
password = models.CharField(max_length=100)
class UserExtension(models.Model):
birthday = models.DateTimeField(null=True)
school = models.CharField(blank=True, max_length=50)
user = models.OneToOneField("User", on_delete=models.CASCADE)
在 UserExtension
模型上增加了一个一对一的关系映射。其实底层是在 UserExtension
这个表上增加了一个 user_id
,来和 user 表进行关联,并且这个外键数据在表中必须是唯一的,来保证一对一。 表名是article_userextension
新增
def add_user(request):
'''
create() 是 模型管理器(Model Manager) 提供的方法,通常用于 直接创建并保存一条记录。
等价于:
user = User(username="张三", password="123456")
user.save()
add() 是用在 多对多关系字段(ManyToManyField) 上的,用于在两个对象之间 建立关系。
'''
# 创建用户后同时创建扩展信息:
user = User.objects.create(username='张三', password='123456')
UserExtension.objects.create(user=user, birthday='2000-01-01', school='北大')
# 也可以通过反向关系创建(推荐):默认情况下,Django 会自动创建一个反向属性,名字就是小写模型名(userextension)。
user = User.objects.create(username='李四', password='abcdef')
user.userextension = UserExtension.objects.create(birthday='1999-05-20', school='清华', user=user)
return HttpResponse("add successfully")
查询
def read_user(request):
# 正向查询:从 UserExtension 获取 User
ext = UserExtension.objects.get(id=1)
print(ext.user.username)
# 反向查询:从 User 获取 UserExtension
user = User.objects.get(id=6)
# 注意反向默认字段名是 model名小写
print(user.userextension.school)
return HttpResponse("read successfully")
修改
# 修改 UserExtension 的 school 字段
ext = UserExtension.objects.get(user_id=1)
ext.school = "复旦大学"
ext.save()
# 修改 User 的密码
user = User.objects.get(id=1)
user.password = "newpassword"
user.save()
删除
# 会自动删除对应的扩展信息,因为用了 on_delete=models.CASCADE
user = User.objects.get(id=1)
user.delete() # 会自动连带删掉 UserExtension
判断是否存在扩展信息
def has_ext(request):
user = User.objects.get(id=4)
if hasattr(user, 'userextension'):
print(user.userextension.school)
else:
print("该用户没有扩展信息")
return HttpResponse("has_ext")
多对多
应用场景:比如文章和标签的关系。一篇文章可以有多个标签,一个标签可以被多个文章所引用。因此标签和文章的关系是典型的多对多的关系。
实现方式: Django 为这种多对多的实现提供了专门的 Field 。叫做 ManyToManyField
。还是拿文章和标签为例进行讲解。示例代码如下:
class Article(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
tags = models.ManyToManyField("Tag", related_name="articles")
class Tag(models.Model):
name = models.CharField(max_length=50)
在数据库层面,实际上 Django 是为这种多对多的关系建立了一个中间表。这个中间表分别定义了两个外键,引用到 article 和 tag 两张表的主键
新增
def add_tag(request):
article = Article.objects.create(title="Django入门", content="内容略",author=User.objects.get(id=5))
tag1 = Tag.objects.create(name="Python")
tag2 = Tag.objects.create(name="Web开发")
# 添加标签(多对多):操作的是关系表 demo.article_article_tags
# 是往 article_article_tags 里面添加数据
article.tags.add(tag1, tag2)
return HttpResponse("add successfully")
以下示例更能体现多对多的关系:
def add_tag2(request):
# 创建标签
python_tag = Tag.objects.create(name="Python")
django_tag = Tag.objects.create(name="Django")
ai_tag = Tag.objects.create(name="AI")
# 创建文章
article1 = Article.objects.create(title="Django入门",
content="Django是一个Python Web框架",
author=User.objects.get(id=5))
article2 = Article.objects.create(title="Python和人工智能",
content="Python在AI中的应用",
author=User.objects.get(id=5))
# 建立多对多关系(文章1关联多个标签)
article1.tags.add(python_tag, django_tag)
# 文章2也关联多个标签,其中包含和文章1重叠的tag(体现“多”的另一侧)
article2.tags.add(python_tag, ai_tag)
# 查询:某篇文章的所有标签
for tag in article1.tags.all():
print("文章1的标签:", tag.name)
# 查询:某个标签关联的所有文章(related_name="articles"时)
for art in python_tag.articles.all():
print("与Python标签相关的文章:", art.title)
return HttpResponse("add successfully")
查询
def read_tag(request):
# 查询某文章的所有标签
article = Article.objects.get(id=16)
tags = article.tags.all()
for tag in tags:
print(tag.name)
# 查询某标签下的所有文章
tag = Tag.objects.get(id=6)
articles = tag.articles.all()
for a in articles:
print(a.title)
return HttpResponse("read successfully")
修改
def update_tag(request):
# 给文章追加一个标签
article = Article.objects.get(id=17)
new_tag = Tag.objects.create(name="English")
article.tags.add(new_tag)
print('-------------------------------------')
# 移除某个标签
tag = Tag.objects.get(id=3)
'''''
DELETE FROM `article_article_tags`
WHERE (`article_article_tags`.`article_id` = 17 AND `article_article_tags`.`tag_id` IN (3));
args=(17, 3);
'''''
article.tags.remove(tag)
print('-------------------------------------')
# 替换标签(先清空再添加)
article2 = Article.objects.get(id=19)
"""""
1- 先找到 id = 19 的文章 所用到的 tag 表的 id :得到 6 和 7
SELECT `article_tag`.`id` FROM `article_tag`
INNER JOIN `article_article_tags`
ON (`article_tag`.`id` = `article_article_tags`.`tag_id`)
WHERE `article_article_tags`.`article_id` = 19;
args=(19,); alias=default
2- 删除
DELETE FROM `article_article_tags`
WHERE (`article_article_tags`.`article_id` = 19
AND `article_article_tags`.`tag_id`
IN (6, 7));
args=(19, 6, 7); alias=default
3- 在插入,也就是关联新的值
INSERT IGNORE INTO `article_article_tags` (`article_id`, `tag_id`)
VALUES (19, 10), (19, 3);
args=(19, 10, 19, 3); alias=default
"""""
article2.tags.set([new_tag, tag])
return HttpResponse("update successfully")
删除
def delete_tag(request):
# 清除某篇文章的所有标签
article = Article.objects.get(id=18)
article.tags.clear()
# 删除标签(数据库级别)
tag_to_delete = Tag.objects.get(id=3)
tag_to_delete.delete() # 这会从Tag表删除该标签,关系自动解除
return HttpResponse("delete successfully")
Save()
,create()
,和 add()
的区别
create()
创建并保存对象,它是 Manager
的方法(通常是 objects
),它会:
- 创建一个模型实例;
- 直接保存到数据库中(相当于自动调用
.save()
); - 返回这个新对象。
article = Article.objects.create(title="Django入门", content="内容略")
# 这个语句等同于:
article = Article(title="Django入门", content="内容略")
article.save()
save()
是模型实例的方法,用于将这个对象保存到数据库中,适用于:
手动创建模型对象后保存;
修改已有对象的字段并保存更新。
article = Article(title="Django进阶", content="内容2")
article.save() # 保存到数据库
article = Article.objects.get(id=1)
article.title = "新标题"
article.save() # 更新数据库中的数据
add()
是 ManyToManyField
字段提供的方法,用于在对象之间建立关系(操作的是关系表),不是直接保存模型本身。
前提是主对象(例如 article
)必须已经保存,否则你会报错。
article.tags.add(tag1, tag2)
它的作用是把 article
和 tag1
、tag2
之间的关系添加到 **中间表(多对多表)**中,而不是创建或保存 Article
或 Tag
对象本身。
方法 | 作用 | 适用于 |
---|---|---|
create() |
创建并保存一个对象 | 创建时直接入库 |
save() |
保存或更新一个模型对象 | 手动创建或修改数据 |
add() |
添加多对多关系(中间表) | 多对多字段关系维护 |
related_name和related_query_name
related_name
还是以 User
和 Article
为例来进行说明。如果一个 article
想要访问对应的作者,那么可以通过author
来进行访问。
但是如果有一个 user
对象,想要通过这个 user
对象获取所有的文章,该如何做呢?
这时候可以通过 user.article_set
来访问,这个名字的规律是 模型名字小写_set
。示例代码如下:
user = User.objects.get(name='张三')
user.article_set.all()
如果不想使用 模型名字小写_set
的方式,想要使用其他的名字,那么可以在定义模型的时候指定related_name
。示例代码如下:
class Article(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
# 传递related_name参数,以后在方向引用的时候使用articles进行访问
author = models.ForeignKey("User",on_delete=models.SET_NULL,null=True,related_name='articles')
以后在反向引用的时候。使用 articles
可以访问到这个作者的文章模型。示例代码如下:
user = User.objects.get(name='张三')
user.articles.all()
如果不想使用反向引用,那么可以指定 related_name='+'
。示例代码如下:
lass Article(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
# 传递related_name参数,以后在方向引用的时候使用articles进行访问
author = models.ForeignKey("User",on_delete=models.SET_NULL,null=True,related_name='+')
以后将不能通过 user.article_set
来访问文章模型了。
related_query_name
在查找数据的时候,可以使用 filter
进行过滤。使用 filter
过滤的时候,不仅仅可以指定本模型上的某个属性要满足什么条件,还可以指定相关联的模型满足什么属性。比如现在想要获取写过标题为 abc
的所有用户,那么可以这样写:
# 查询的是 user, 但是 查询条件 用的是 article 的 title
users = User.objects.filter(article__title='abc')
上面使用的是默认的方向关系名:模型名称的全部小写.对应的Filed
如果你设置了 related_name
为 articles
,因为反转的过滤器的名字将使用 related_name
的名字,那么上例代码将改成如下:
users = User.objects.filter(articles__title='abc') # 这里是articles, 上面是article
可以通过 related_query_name
将查询的反转名字修改成其他的名字。比如 article 。示例代码如下:
class Article(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
# 传递related_name参数,以后在方向引用的时候使用articles进行访问
author=models.ForeignKey("User",on_delete=models.SET_NULL,null=True,
related_name='articles',related_query_name='aaa')
那么在做反向过滤查找的时候就可以使用以下代码:
users = User.objects.filter(aaa__title='abc')
related_query_name 的参数规范:
model__field__lookup='xxxx'
model
:
- 小写模型名: 如果模型叫Article,那么 model = article
- related_name指定的名字: 如果related_name = abc,那么 model = abc
- related_query_name指定的名字:如果related_query_name=bbb,那么 model=bbb
__
(双下划线):Django 的特殊语法,表示”跳转到关联模型”或”进入字段的查找类型”
field
:关联模型中的字段名(如 title
、content
等)
lookup
(可选)
- 查询条件类型(如
icontains
、gt
、exact
等) - 如果省略,默认为
exact
精确匹配
区别
属性 | 用途 | 使用场景 | 举例 |
---|---|---|---|
related_name |
设置反向访问时的属性名 | 通过对象属性访问外键关联对象 | category.articles.all() |
related_query_name |
设置反向查询时的查询名 | 用于filter 查询中的字段名 | Category.objects.filter(articles__title="...") |
查询操作
filter
、 exclude
以及 get
查询一般就是使用 filter
、 exclude
以及 get
三个方法来实现。
方法 | 返回类型 | 多条记录 | 不存在记录时 | 查询多条时报错 |
---|---|---|---|---|
filter |
QuerySet | 多条记录 | 返回空QuerySet | 不会报错 |
exclude |
QuerySet | 多条记录 | 返回全部(不排除) | 不会报错 |
get |
单个对象 | 只能一条 | 抛出DoesNotExist异常 | 抛出 MultipleObjectsReturned 异常 |
filter()
作用:根据条件筛选出符合条件的对象集合。
返回值:一个
QuerySet
(可以理解为一个对象列表),即使只有一个匹配结果。使用场景:你希望获得满足某个条件的一组对象。
exclude()
- 作用:排除掉符合某些条件的对象,返回剩下的。
- 返回值:一个
QuerySet
。 - 使用场景:你希望获得不满足某个条件的对象集合
get()
- 作用:获取单个满足条件的对象。
- 返回值:一个模型实例(单个对象),不是
QuerySet
。 - 注意事项:如果查询到多个结果,会抛出
MultipleObjectsReturned
异常; 如果没有任何结果,会抛出DoesNotExist
异常。 - 使用场景:你确信只会返回一个对象,比如主键查询
查询条件
在 ORM 层面,这些查询条件都是使用 field + __ + condition
的方式来使用的。以下将那些常用的查询条件来一一解释。
exact
使用精确的 =
进行查找。如果提供的是一个 None ,那么在 SQL 层面就是被解释为 NULL 。示例代码如下:
article = Article.objects.get(id__exact=14)
article = Article.objects.get(id__exact=None)
以上的两个查找在翻译为 SQL 语句为如下:
select ... from article where id=14;
select ... from article where id IS NULL;
iexact
使用 like
进行查找。示例代码如下
article = Article.objects.filter(title__iexact='hello world')
那么以上的查询就等价于以下的 SQL
语句:
select ... from article where title like 'hello world';
注意上面这个 sql 语句,因为在 MySQL 中,没有一个叫做 ilike
的。所以 exact
和 iexact
的区别实际上就是 LIKE
和 =
的区别
MySql:Like 是否区分大小写
数据库系统 | LIKE 是否区分大小写 |
解决方案(忽略大小写) |
---|---|---|
MySQL | 默认不区分(取决于 collation) | 使用 utf8_general_ci 或 LOWER() |
PostgreSQL | 区分 | 使用 ILIKE 或 LOWER() |
SQLite | 默认不区分 | 若需区分用 GLOB |
Oracle | 区分 | 使用 UPPER() 或更改 NLS 设置 |
MySql:Collation
Collation 是“字符集的比较规则”,决定了:
- 字符串的比较是否区分大小写(
a
和A
是不是一样) - 字符串排序的方式(
Z
排在a
前还是后)
假设你有一张表:
CREATE TABLE users (
name VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci
);
utf8
是 字符集(决定能存哪些字符,比如中文、日文、emoji 等)
utf8_general_ci
是 排序规则(collation)
ci
= case-insensitive(不区分大小写)- 所以:
LIKE 'john'
会匹配John
,JOHN
,john
但如果是:
COLLATE utf8_bin
bin
= binary(二进制比较)- 完全区分大小写,所以
LIKE 'john'
只匹配john
,不匹配JOHN
常见的 Collation 后缀含义:
后缀 | 意思 | 是否区分大小写 |
---|---|---|
_ci |
case-insensitive | 不区分 |
_cs |
case-sensitive | 区分 |
_bin |
binary(按字节比对) | 区分 |
不同版本的默认设置:
MySQL 版本 | 默认字符集 | 默认排序规则 |
---|---|---|
MySQL 5.5 及以前 | latin1 |
latin1_swedish_ci |
MySQL 5.6 / 5.7 | utf8 |
utf8_general_ci |
MySQL 8.0+ | utf8mb4 (推荐) |
utf8mb4_0900_ai_ci |
utf8
在 MySQL 中 并不是真正意义上的 UTF-8,最多只能存 3 字节 的字符,无法存储某些 emoji 或偏僻文字。
utf8mb4
才是完整支持 UTF-8 的字符集(最多 4 字节),建议始终使用 utf8mb4
。
contains
大小写敏感,判断某个字段是否包含了某个数据。示例代码如下:
articles = Article.objects.filter(title__contains='hello')
在翻译成 SQL 语句为如下:
select ... where title like binary '%hello%';
要注意的是,在使用 contains
的时候,翻译成的 sql 语句左右两边是有百分号的,意味着使用的是模糊查询。而 exact
翻译成 sql 语句左右两边是没有百分号的,意味着使用的是精确的查询。
icontains
大小写不敏感的匹配查询。示例代码如下:
articles = Article.objects.filter(title__icontains='hello')
在翻译成 SQL 语句为如下:
select ... where title like '%hello%';
MySql:BINARY
BINARY
是一种 类型转换,它让字符串变成按字节比较的方式,跳过字符集的 collation 规则(比如 utf8_general_ci
中的 ci
= case-insensitive)
LIKE BINARY
用来让 LIKE
强制区分大小写,无论字段的 collation 是不是 _ci
(不区分大小写)。
in:
提取那些给定的 field 的值是否在给定的容器中。容器可以为 list
、 tuple
或者任何一个可以迭代的对象,包括 QuerySet 对象。示例代码如下:
articles = Article.objects.filter(id__in=[1,2,3])
以上代码在翻译成 SQL 语句为如下:
select ... where id in (1,3,4)
当然也可以传递一个 QuerySet 对象进去。示例代码如下
inner_qs = Article.objects.filter(title__contains='hello')
tags = Tag.objects.filter(id__in=inner_qs)
以上代码的意思是获取那些文章标题包含 hello 的所有分类。
将翻译成以下 SQL 语句,示例代码如下:
SELECT `article_tag`.`id`, `article_tag`.`name` FROM `article_tag` WHERE `article_tag`.`id` IN
(SELECT U0.`id` FROM `article_article` U0 WHERE U0.`title` LIKE BINARY %hello%)
gt
某个 field 的值要大于(greater than)给定的值。示例代码如下:
articles = Article.objects.filter(id__gt=4)
以上代码的意思是将所有 id 大于4的文章全部都找出来。
将翻译成以下 SQL 语句:
select ... where id > 4;
gte
类似于 gt ,是大于等于。
lt
类似于 gt 是小于。
lte
类似于 lt ,是小于等于。
startswith:
判断某个字段的值是否是以某个值开始的。大小写敏感。示例代码如下:
articles = Article.objects.filter(title__startswith='hello')
以上代码的意思是提取所有标题以 hello 字符串开头的文章。
将翻译成以下 SQL 语句:
select ... where title like BINARY 'hello%'
istartswith
类似于 startswith ,但是大小写是不敏感的。
endswith:
判断某个字段的值是否以某个值结束。大小写敏感。示例代码如下:
articles = Article.objects.filter(title__endswith='world')
以上代码的意思是提取所有标题以 world 结尾的文章。
将翻译成以下 SQL 语句
select ... where title LIKE BINARY '%world';
iendswith
类似于 endswith ,只不过大小写不敏感。
range
判断某个 field 的值是否在给定的区间中。示例代码如下:
from django.utils.timezone import make_aware
from datetime import datetime
start_date = make_aware(datetime(year=2018, month=1, day=1))
end_date = make_aware(datetime(year=2018, month=3, day=29, hour=16))
# <class 'datetime.datetime'> 2018-01-01 00:00:00+00:00
# <class 'datetime.datetime'> 2018-03-29 16:00:00+00:00
print(type(start_date), start_date, type(end_date), end_date)
articles = Book.objects.filter(pub_time__range=(start_date, end_date))
# SELECT * FROM `book_model` WHERE `pub_time` BETWEEN 2018-01-01 00:00:00 AND 2018-03-29 16:00:00
print(articles.query)
将翻译成以下的 SQL 语句:
SELECT * FROM `book_model` WHERE `pub_time` BETWEEN 2018-01-01 00:00:00 AND 2018-03-29 16:00:00
需要注意的是,以上提取数据,不会包含最后一个值。也就是不会包含 2018/12/12 的文章。
date
针对某些 date
或者 datetime
类型的字段。可以指定 date
的范围。并且这个时间过滤,还可以使用链式调用。示例代码如下:
articles = Article.objects.filter(pub_date__date=date(2018,3,29))
以上代码的意思是查找时间为 2018/3/29
这一天发表的所有文章。
将翻译成以下的 sql 语句:
select ... WHERE DATE(CONVERT_TZ(`front_article`.`pub_date`, 'UTC', 'Asia/Shanghai')) = 2018-03-29
year
根据年份进行查找。示例代码如下:
articles = Article.objects.filter(pub_date__year=2018)
articles = Article.objects.filter(pub_date__year__gte=2017)
以上的代码在翻译成 SQL 语句为如下
select ... where pub_date between '2018-01-01' and '2018-12-31';
select ... where pub_date >= '2017-01-01';
month
同 year ,根据月份进行查找。
day
同 year ,根据日期进行查找。
week_day:
Django 1.11
新增的查找方式。同 year ,根据星期几进行查找。1
表示星期天,7
表示星期六, 2-6
代表的是星期一到星期五。
time:
根据时间进行查找。示例代码如下:
articles = Article.objects.filter(pub_date__time=datetime.time(12,12,12));
以上的代码是获取每一天中12点12分12秒发表的所有文章。
更多的关于时间的过滤,请参考 Django 官方文档:https://docs.djangoproject.com/en/5.0/ref/models/querysets/#range。
isnull
根据值是否为空进行查找。示例代码如下:
articles = Article.objects.filter(pub_date__isnull=False)
以上的代码的意思是获取所有发布日期不为空的文章。
将来翻译成 SQL 语句如下:
select ... where pub_date is not null;
regex和iregex
大小写敏感和大小写不敏感的正则表达式。iregex
是大小写不敏感的。
示例代码如下:
articles = Article.objects.filter(title__regex=r'^hello')
以上代码的意思是提取所有标题以 hello 字符串开头的文章。
将翻译成以下的 SQL 语句:
select ... where title regexp binary '^hello';
根据关联的表进行查询
假如现在有两个 ORM 模型,一个是 Article ,一个是 Category 。代码如下:
class Category(models.Model):
"""文章分类表"""
name = models.CharField(max_length=100)
class Article(models.Model):
"""文章表"""
title = models.CharField(max_length=100,null=True)
category = models.ForeignKey("Category",on_delete=models.CASCADE)
比如想要获取文章标题中包含"hello"
的所有的分类。那么可以通过以下代码来实现:
categories = Category.objects.filter(article__title__contains="hello")
执行的SQL如下:
SELECT `article_category`.`id`, `article_category`.`name`
FROM `article_category`
INNER JOIN `article_article`
ON (`article_category`.`id` = `article_article`.`category_id`)
WHERE `article_article`.`title` LIKE BINARY %hello%
聚合函数
如果你用原生 SQL ,则可以使用聚合函数来提取数据。比如提取某个商品销售的数量,那么可以使用 Count
,如果想要知道商品销售的平均价格,那么可以使用 Avg
。
聚合函数是通过 aggregate
方法来实现的。在讲解这些聚合函数的用法的时候,都是基于以下的模型对象来实现的。
准备工作
from django.db import models
# Create your models here.
class Author(models.Model):
"""作者模型"""
name = models.CharField(max_length=100)
age = models.IntegerField()
email = models.EmailField()
class Meta:
db_table = 'front_author'
class Publisher(models.Model):
"""出版社模型"""
name = models.CharField(max_length=300)
class Meta:
db_table = 'front_publisher'
class Book(models.Model):
"""图书模型"""
name = models.CharField(max_length=300)
pages = models.IntegerField()
price = models.FloatField()
rating = models.FloatField()
author = models.ForeignKey(Author, on_delete=models.CASCADE)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
class Meta:
db_table = 'front_book'
class BookOrder(models.Model):
"""图书订单模型"""
book = models.ForeignKey("Book", on_delete=models.CASCADE)
price = models.FloatField()
class Meta:
db_table = 'front_book_order'
Avg :求平均值
比如想要获取所有图书的价格平均值。那么可以使用以下代码实现。
from django.db.models import Avg
result = Book.objects.aggregate(Avg('price'))
print(result)
print(connection.queries[-1]['sql'])
以上的打印结果是:
{"price__avg":23.0}
SELECT AVG(`front_book`.`price`) AS `price__avg` FROM `front_book`
其中 price__avg
的结构是根据 field__avg
规则构成的。如果想要修改默认的名字,那么可以将 Avg 赋值给一个关键字参数。示例代码如下:
from django.db.models import Avg
result = Book.objects.aggregate(my_avg=Avg('price'))
print(result)
那么以上的结果打印为:
{"my_avg":23}
Count :获取指定的对象的个数
示例代码如下:
from django.db.models import Count
# SELECT COUNT(`front_book`.`id`) AS `book_num` FROM `front_book`
result = Book.objects.aggregate(book_num=Count('id'))
以上的 result
将返回 Book 表中总共有多少本图书。
Count
类中,还有另外一个参数叫做 distinct
,默认是等于 False
,如果是等于 True
,那么将去掉那些重复的值。比如要获取作者表中所有的不重复的邮箱总共有多少个,那么可以通过以下代码来实现:
from djang.db.models import Count
# SELECT COUNT(DISTINCT `front_author`.`email`) AS `count` FROM `front_author`
result = Author.objects.aggregate(count=Count('email',distinct=True))
如果想要知道总共有多少条数据,那么建议使用 count
,而不是使用 len(articles)
这种。因为 count
在底层是使用 select count(*)
来实现的,这种方式比使用 len
函数更加的高效。
Max
和Min
:获取指定对象的最大值和最小值
比如想要获取 Author 表中,最大的年龄和最小的年龄分别是多少。那么可以通过以下代码来实现:
from django.db.models import Max,Min
# SELECT MAX(`front_author`.`age`) AS `age__max`, MIN(`front_author`.`age`) AS `age__min` FROM `front_author`
result = Author.objects.aggregate(Max('age'),Min('age'))
如果最大的年龄是88,最小的年龄是18。那么以上的result将为:
{"age__max":88,"age__min":18}
Sum
:求指定对象的总和
比如要求图书的销售总额。那么可以使用以下代码实现:
from djang.db.models import Sum
result = Book.objects.annotate(total=Sum("bookorder__price")).values("name","total")
以上的代码 annotate
的意思是给 Book 表在查询的时候添加一个字段叫做 total
,这个字段的数据来源是从 BookOrder 模型的 price
的总和而来。 values
方法是只提取 name
和 total
两个字段的值。
aggregate
和annotate
的区别
# 比较 aggregate和annotate的区别
result = Book.objects.aggregate(Avg('price'))
# aggregate: <class 'dict'>
print("aggregate:",type(result))
# result: {'price__avg': 97.25}
print("result:", result)
# SQL: SELECT AVG(`front_book`.`price`) AS `price__avg` FROM `front_book`
print("SQL:", connection.queries[-1]['sql'])
print("**" * 30)
result2 = Book.objects.annotate(avg_price=Avg('price')).values('avg_price')
# aggregate: <class 'django.db.models.query.QuerySet'>
print("aggregate:",type(result2))
for result in result2:
# result: {'avg_price': 98.0}
# result: {'avg_price': 97.0}
# result: {'avg_price': 95.0}
# result: {'avg_price': 99.0}
print("result:", result)
# SQL: SELECT AVG(`front_book`.`price`) AS `avg_price` FROM `front_book`
# GROUP BY `front_book`.`id` ORDER BY NULL
print("SQL:", connection.queries[-1]['sql'])
print("**" * 30)
return HttpResponse("index success")
特点 | aggregate() |
annotate() |
---|---|---|
聚合范围 | 整个 QuerySet | 每个对象(类似分组) |
返回类型 | 字典 | QuerySet(每个对象加了字段) |
常见用途 | 总数、平均数、最大/最小值等 | 统计每个用户、作者、分类下的对象数量等 |
类似 SQL | SELECT AVG(...) FROM table |
SELECT ..., COUNT(...) GROUP BY ... |
annotate()
的分组问题
你写的代码 | Django 默认的分组行为 |
---|---|
.annotate(...) |
按主模型主键(比如 id )分组 |
.values('field') |
按你指定的字段分组 |
.values('field1', 'field2') |
多字段分组 |
默认根据(主模型的)主键进行分组:
# 默认根据(主模型的)主键进行分组
results = Book.objects.annotate(total=Sum("bookorder__price"))
for result in results:
print(result)
# SELECT * , SUM(`front_book_order`.`price`) AS `total` FROM `front_book`
# LEFT OUTER JOIN `front_book_order`
# ON (`front_book`.`id` = `front_book_order`.`book_id`)
# GROUP BY `front_book`.`id` ORDER BY NULL
print(results.query)
return HttpResponse("index success")
写法 | 分组字段 | 行为 |
---|---|---|
① .values('book_id') → .annotate(...) |
book_id |
按 book_id 分组,然后聚合(这是你想要的) |
② .annotate(...) → .values(...) |
默认是主键 id | 按 id 分组,结果是一行一条记录,count(book_id) 总是为 1 |
''' -
SELECT `front_book_order`.`book_id`, COUNT(`front_book_order`.`book_id`) AS `count`
FROM `front_book_order`
GROUP BY `front_book_order`.`id`
ORDER BY NULL
{'book_id': 1, 'count': 1}
{'book_id': 1, 'count': 1}
{'book_id': 1, 'count': 1}
{'book_id': 2, 'count': 1}
{'book_id': 2, 'count': 1}
'''
books=BookOrder.objects.annotate(count=Count("book_id")).values('count', 'book_id')
for book in books:
print(book)
print(books.query)
'''-
SELECT `front_book_order`.`book_id`, COUNT(`front_book_order`.`book_id`) AS `count`
FROM `front_book_order`
GROUP BY `front_book_order`.`book_id`
ORDER BY NULL
{'book_id': 1, 'count': 3}
{'book_id': 2, 'count': 2}
'''
books2=BookOrder.objects.values('book_id').annotate(count=Count("book_id"))
for book in books2:
print(book)
print(books2.query)
多字段分组:.values('field1', 'field2')
'''-
SELECT `front_book`.`rating`, `front_book`.`publisher_id`, COUNT(`front_book`.`id`) AS `count`
FROM `front_book`
GROUP BY `front_book`.`rating`, `front_book`.`publisher_id`
ORDER BY NULL
{'rating': 4.8, 'publisher_id': 1, 'count': 2}
{'rating': 4.8, 'publisher_id': 2, 'count': 1}
{'rating': 4.9, 'publisher_id': 2, 'count': 1}
'''
books3=Book.objects.values('rating','publisher_id').values(count=Count("id"))
.values('rating','publisher_id','count')
for book in books3:
print(book)
print(books3.query)
return HttpResponse("index success")
错误的分组:
results = Book.objects.annotate(total=Sum("bookorder__price"))
for result in results:
print(result)
'''-
SELECT `front_book`.`id`, `front_book`.`name`, `front_book`.`pages`,
`front_book`.`price`, `front_book`.`rating`, `front_book`.`author_id`,
`front_book`.`publisher_id`, SUM(`front_book_order`.`price`) AS `total`
FROM `front_book`
LEFT OUTER JOIN `front_book_order`
ON (`front_book`.`id` = `front_book_order`.`book_id`)
GROUP BY `front_book`.`id` ORDER BY NULL
'''
print(results.query)
results2 = Book.objects.annotate(count=Count("bookorder__book_id")).values('count', 'bookorder__book_id')
for result in results2:
print(result)
''' -
SELECT `front_book_order`.`book_id`, COUNT(`front_book_order`.`book_id`) AS `count`
FROM `front_book`
LEFT OUTER JOIN `front_book_order`
ON (`front_book`.`id` = `front_book_order`.`book_id`)
GROUP BY `front_book`.`id`, `front_book_order`.`book_id`
ORDER BY NULL
{'bookorder__book_id': None, 'count': 0}
{'bookorder__book_id': None, 'count': 0}
{'bookorder__book_id': 1, 'count': 3}
{'bookorder__book_id': 2, 'count': 2}
'''
print(results2.query)
第一段:没有用 .values()
,只用了 .annotate()
。Django 默认按主模型的主键 front_book
.id
分组
第二段:用了 .values()
,但.annotate()
在前,.values()
在后,Django 默认 Book.id
+ values指定的字段
进行多字段分组。
如果真的只想按 bookorder__book_id 分组:
results3 = Book.objects.values('bookorder__book_id').annotate(count=Count("bookorder__book_id"))
.values('count', 'bookorder__book_id')
for result in results3:
print(result)
''' -
SELECT `front_book_order`.`book_id`, COUNT(`front_book_order`.`book_id`) AS `count`
FROM `front_book` LEFT OUTER JOIN `front_book_order`
ON (`front_book`.`id` = `front_book_order`.`book_id`)
GROUP BY `front_book_order`.`book_id` ORDER BY NULL
{'bookorder__book_id': None, 'count': 0}
{'bookorder__book_id': 1, 'count': 3}
{'bookorder__book_id': 2, 'count': 2}
'''
print(results3.query)
F表达式
F表达式的使用
Django 的 F()
表达式 是 ORM 查询中的一大利器,可以让你在数据库层面做字段之间的比较、更新、自增等操作,而不需要先取出值再计算,极大提高性能。
字段比较
例如:查询价格高于库存数量的书籍:
from django.db.models import F
Book.objects.filter(price__gt=F('stock'))
这会被翻译成类似 SQL:
SELECT * FROM book WHERE price > stock;
字段的运算
给某个字段直接加 1(如点击量、库存数):
Book.objects.filter(id=1).update(stock=F('stock') - 1)
这不会先查再写,而是直接执行:
UPDATE book SET stock = stock - 1 WHERE id = 1;
这比下面这种写法高效得多(后者是先查再写,存在并发问题):
book = Book.objects.get(id=1)
book.stock -= 1
book.save()
在 annotate()
中使用字段表达式
比如想计算打 8 折的价格:
Book.objects.annotate(discounted_price=F('price') * 0.8)
实际执行的SQL
SELECT
`book`.`id`,
`book`.`price`,
(`book`.`price` * 0.8) AS `discounted_price`
FROM `book`
访问关联表字段
results = Book.objects.annotate(upprice=F('bookorder__price')+10).filter(upprice__gt=100)
相关SQL
SELECT `front_book`.`id`, `front_book`.`name`, `front_book`.`pages`, `front_book`.`price`, `front_book`.`rating`, `front_book`.`author_id`, `front_book`.`publisher_id`, (`front_book_order`.`price` + 10) AS `upprice` FROM `front_book`
LEFT OUTER JOIN `front_book_order`
ON (`front_book`.`id` = `front_book_order`.`book_id`)
WHERE (`front_book_order`.`price` + 10) > 100.0
总结
F()
表达式表示“字段本身的值”,它可以用于字段之间的比较和运算,最终会被翻译成 SQL 语句的一部分。
被调用的时候才执行
F 表达式不是立即执行,而是在调用 ORM 方法时执行
expr = F('price') * 1.1
print(expr) # F(price) * Value(1.1)
它没有对数据库做任何操作,没有 SQL 被执行,也没有计算出新的价格。
这说明:定义表达式时只是构建对象,不执行操作
queryset = Book.objects.filter(price__gt=F("cost"))
print(queryset)
# 这时候依旧没有执行 SQL,只是构造了一个 QuerySet。
# 遍历触发查询, 才会真正执行F表达式
for book in queryset:
print(book.title)
F 表达式是“懒执行”的:只在 .update()
、.filter()
、.annotate()
等 ORM 方法执行时,才会触发计算和 SQL 生成。
为什么要用 F 表达式
原子性:直接在数据库层做更新、计算,更安全(不会出现并发覆盖问题)
效率高:不需要取数据到 Python 里再更新,直接 SQL 完成任务
表达力强:可以在
.filter()
、.annotate()
、.update()
里使用,表达逻辑更自然
Pyhton与Java的对比
Python的写法:
Book.objects.filter(id=1).update(stock=F('stock') - 1)
Java 的写法:
Mapper 接口:
int decreaseStock(@Param("id") int bookId);
XML:
<update id="decreaseStock" parameterType="int">
UPDATE book
SET stock = stock - 1
WHERE id = #{id} AND stock > 0
</update>
对比:
特性 | Django F 表达式 | Java + MyBatis 原生 SQL |
---|---|---|
开发效率 | 高:自动生成 SQL,快速实现原子操作 | 中:需要手写 SQL,适合复杂场景 |
性能 | 中:ORM 层有一定的性能损耗 | 高:手写 SQL 可针对性优化 |
灵活性 | 低:ORM 层对于复杂 SQL 的支持有限 | 高:完全掌控 SQL 语句,适应复杂需求 |
并发安全性 | 高:自动处理事务,确保原子性操作 | 中:需要开发者手动管理事务,容易出错 |
可维护性 | 高:代码简洁,ORM 会自动更新 SQL | 低:SQL 需要手动管理,修改数据库时可能涉及大量 SQL 修改 |
跨数据库兼容性 | 高:跨数据库支持良好,自动处理数据库差异 | 低:每个数据库的 SQL 可能不同,需要手动调整 |
Q表达式
如果想要实现所有价格高于100元,并且评分达到9.0以上评分的图书。那么可以通过以下代码来实现:
books = Book.objects.filter(price__gte=100,rating__gte=9)
以上这个案例是一个并集查询,可以简单的通过传递多个条件进去来实现。
但是如果想要实现一些复杂的查询语句,比如要查询所有价格低于10元,或者是评分低于9分的图书。那就没有办法通过传递多个条件进去实现了。这时候就需要使用 Q表达式 来实现了。示例代码如下:
from django.db.models import Q
books = Book.objects.filter(Q(price__lte=10) | Q(rating__lte=9))
以上是进行或运算,当然还可以进行其他的运算,比如有 &
和 ~
(非) 等。一些用 Q 表达式的例子如下:
from django.db.models import Q
# 获取id等于3的图书
books = Book.objects.filter(Q(id=3))
# 获取id等于3,或者名字中包含文字"记"的图书
books = Book.objects.filter(Q(id=3)|Q(name__contains("记")))
# 获取价格大于100,并且书名中包含"记"的图书
books = Book.objects.filter(Q(price__gte=100)&Q(name__contains("记")))
# 获取书名包含“记”,但是id不等于3的图书 : ~Q(id=3)
books = Book.objects.filter(Q(name__contains='记') & ~Q(id=3))
QuerySet API
我们通常做查询操作的时候,都是通过 模型名字.objects
的方式进行操作。其实 模型名字.objects
是一个 django.db.models.manager.Manager
对象,而 Manager
这个类是一个“空壳”
的类,他本身是没有任何的属性和方法的。他的方法全部都是通过 Python 动态添加的方式,从 QuerySet
类中拷贝过来的。示例图如下
返回新的QuerySet的方法
在使用 QuerySet 进行查找操作的时候,可以提供多种操作。比如过滤完后还要根据某个字段进行排序,那么这一系列的操作我们可以通过一个非常流畅的 链式调用
的方式进行。比如要从文章表中获取标题为123 ,并且提取后要将结果根据发布的时间进行排序,那么可以使用以下方式来完成:
articles = Article.objects.filter(title='123').order_by('create_time')
可以看到 order_by
方法是直接在 filter 执行后调用的。这说明 filter
返回的对象是一个拥有order_by
方法的对象。而这个对象正是一个新的 QuerySet
对象。因此可以使用 order_by
方法。
常用的QuerySet方法
已经学过的方法
filter
; exclude
; annotate
; order_by
; all
; get
; create
; count
; aggregate
;
values
values
:用来指定在提取数据出来,需要提取哪些字段。默认情况下会把表中所有的字段全部都提取出来,可以使用 values
来进行指定,并且使用了 values
方法后,提取出的 QuerySet
中的数据类型不是模型,而是在 values
方法中指定的字段和值形成的字典:
articles = Article.objects.values("title",'content')
for article in articles:
print(article)
以上打印出来的 article 是类似于 {"title":"abc","content":"xxx"}
的形式。
如果在 values
中没有传递任何参数,那么将会返回这个恶模型中所有的属性。
values_list
values_list
:类似于 values
。只不过返回的 QuerySet
中,存储的不是字典,而是元组。示例代码如下:
articles = Article.objects.values_list("id","title")
print(articles)
那么在打印 articles 后,结果为 <QuerySet [(1,'abc'),(2,'xxx'),...]>
等。
如果在 values_list
中只有一个字段。那么你可以传递 flat=True
来将结果扁平化。示例代码如下:
articles1 = Article.objects.values_list("title")
>> <QuerySet [("abc",),("xxx",),...]>
articles2 = Article.objects.values_list("title",flat=True)
>> <QuerySet ["abc",'xxx',...]>
select_related
select_related
:在提取某个模型的数据的同时,也提前将相关联的数据提取出来。比如提取文章数据,可以使用 select_related
将 author
信息提取出来,以后再次使用 article.author
的时候就不需要再次去访问数据库了。可以减少数据库查询的次数。示例代码如下:
article = Article.objects.get(pk=1)
>> article.author # 重新执行一次查询语句
article = Article.objects.select_related("author").get(pk=2)
>> article.author # 不需要重新执行查询语句了
select_related
只能用在 一对多
或者 一对一
中,不能用在 多对多
或者 多对一
中。比如可以提前获取文章的作者,但是不能通过作者获取他的文章,或者是通过某篇文章获取这个文章所有的标签。
prefetch_related
prefetch_related
:这个方法和 select_related
非常的类似,就是在访问多个表中的数据的时候,减少查询的次数。这个方法是为了解决 多对一
和 多对多
的关系的查询问题。比如要获取标题中带有 hello
字符串的文章以及他的所有标签,示例代码如下
from django.db import connection
articles = Article.objects.prefetch_related("tag_set")
.filter(title__contains='hello')
print(articles.query) # 通过这条命令查看在底层的SQL语句
for article in articles:
print("title:",article.title)
print(article.tag_set.all())
# 通过以下代码可以看出以上代码执行的sql语句
for sql in connection.queries:
print(sql)
但是如果在使用 article.tag_set
的时候,如果又创建了一个新的 QuerySet 那么会把之前的 SQL优化给破坏掉。比如以下代码:
tags = Tag.obejcts.prefetch_related("articles")
for tag in tags:
#因为filter方法会重新生成一个QuerySet,因此会破坏掉之前的sql优化
articles = tag.articles.filter(title__contains='hello')
# 通过以下代码,我们可以看到在使用了filter的,他的sql查询会更多,而没有使用filter的,只有两次sql查询
for sql in connection.queries:
print(sql)
那如果确实是想要在查询的时候指定过滤条件该如何做呢,这时候我们可以使用django.db.models.Prefetch
来实现, Prefetch
这个可以提前定义好 queryset
。示例代码如下:
tags = Tag.objects.prefetch_related(
Prefetch("articles",queryset=Article.objects.filter(title__contains='hello'))
).all()
for tag in tags:
articles = tag.articles.all()
for article in articles:
print(article)
for sql in connection.queries:
print('='*30)
print(sql)
因为使用了 Prefetch
,即使在查询文章的时候使用了 filter
,也只会发生两次查询操作。
defer
defer
:在一些表中,可能存在很多的字段,但是一些字段的数据量可能是比较庞大的,而此时你又不需要,比如我们在获取文章列表的时候,文章的内容我们是不需要的,因此这时候我们就可以使用 defer
来过滤掉一些字段。这个字段跟 values
有点类似,只不过 defer
返回的不是字典,而是模型。示例代码如下:
articles = list(Article.objects.defer("title"))
for sql in connection.queries:
print('='*30)
print(sql)
在看以上代码的 sql 语句,你就可以看到,查找文章的字段,除了 title
,其他字段都查找出来了。当然,你也可以使用 article.title
来获取这个文章的标题,但是会重新执行一个查询的语句。示例代码如下:
articles = list(Article.objects.defer("title"))
for article in articles:
# 因为在上面提取的时候过滤了title
# 这个地方重新获取title,将重新向数据库中进行一次查找操作
print(article.title)
for sql in connection.queries:
print('='*30)
print(sql)
article = Article(title='abc')
article.save()
# 下面这行代码相当于以上两行代码
article = Article.objects.create(title='abc')
defer
虽然能过滤字段,但是有些字段是不能过滤的,比如 id ,即使你过滤了,也会提取出来。
only
only
:跟 defer
类似,只不过 defer 是过滤掉指定的字段,而 only 是只提取指定的字段。
get_or_create
get_or_create
: 根据某个条件进行查找,如果找到了那么就返回这条数据,如果没有查找到,那么就创建一个。示例代码如下:
obj,created= Category.objects.get_or_create(title='默认分类')
如果有标题等于 默认分类
的分类,那么就会查找出来,如果没有,则会创建并且存储到数据库中。这个方法的返回值是一个元组,元组的第一个参数 obj
是这个对象,第二个参数 created
代表是否创建的
bulk_create
bulk_create
: 一次性创建多个数据。示例代码如下:
Tag.objects.bulk_create([
Tag(name='111'),
Tag(name='222'),
])
first
和 last
first
和 last
:返回 QuerySet
中的第一条和最后一条数据
exists
exists
: 判断某个条件的数据是否存在。如果要判断某个条件的元素是否存在,那么建议使用exists
,这比使用 count
或者直接判断 QuerySet
更有效得多。示例代码如下:
if Article.objects.filter(title__contains='hello').exists():
print(True)
# 比使用count更高效:
if Article.objects.filter(title__contains='hello').count() > 0:
print(True)
# 也比直接判断QuerySet更高效:
if Article.objects.filter(title__contains='hello'):
print(True)
distinct
distinct
:去除掉那些重复的数据。这个方法如果底层数据库用的是 MySQL ,那么不能传递任何的参数。比如想要提取所有销售的价格超过80元的图书,并且删掉那些重复的,那么可以使用distinct
来帮我们实现,示例代码如下:
books = Book.objects.filter(bookorder__price__gte=80).distinct()
需要注意的是,如果在 distinct
之前使用了 order_by
,那么因为 order_by
会提取 order_by
中指定的字段,因此再使用 distinct
就会根据多个字段来进行唯一化,所以就不会把那些重复的数据删掉。示例代码如下:
orders = BookOrder.objects.order_by("create_time").values("book_id").distinct()
那么以上代码因为使用了 order_by
,即使使用了 distinct
,也会把重复的 book_id
提取出来。
update
update
:执行更新操作,在 SQL 底层走的也是 update
命令。比如要将所有 category
为空的article
的 article
字段都更新为默认的分类。示例代码如下:
Article.objects.filter(category__isnull=True).update(category_id=3)
注意这个方法走的是更新的逻辑。所以更新完成后保存到数据库中不会执行 save 方法,因此不会更新 auto_now
设置的字段。
delete
delete
:删除所有满足条件的数据。删除数据的时候,要注意 on_delete
指定的处理方式。
切片操作
切片操作
:有时候我们查找数据,有可能只需要其中的一部分。那么这时候可以使用切片操作来帮我们完成。 QuerySet
使用切片操作就跟列表使用切片操作是一样的。示例代码如下:
books = Book.objects.all()[1:3]
for book in books:
print(book)
切片操作
并不是把所有数据从数据库中提取出来再做切片操作。而是在数据库层面使用 LIMIE
和 OFFSET
来帮我们完成。所以如果只需要取其中一部分的数据的时候,建议大家使用切片操作
。
什么时候Django会将QuerySet转换为SQL去执行
问题引入:
result = Book.objects.annotate(total=Sum("bookorder__price")).values("name", "total")
print(result)
print(result.query)
# SELECT AVG(`front_book`.`price`) AS `price__avg` FROM `front_book`
print(connection.queries[-1]['sql'])
以上代码中,
生成一个 QuerySet
对象并不会马上转换为 SQL
语句去执行。
比如我们获取 Book
表下所有的图书:
books = Book.objects.all()
print(connection.queries)
我们可以看到在打印 connection.quries
的时候打印的是一个空的列表。说明上面的 QuerySet
并没有真正的执行。
在以下情况下 QuerySet
会被转换为 SQL
语句执行:
迭代:在遍历
QuerySet
对象的时候,会首先先执行这个SQL
语句,然后再把这个结果返回进行迭代。比如以下代码就会转换为 SQL 语句:for book in Book.objects.all(): print(book)
使用步长做
切片操作
:QuerySet
可以类似于列表一样做切片操作。做切片操作本身不会执行 SQL语句,但是如果如果在做切片操作的时候提供了步长,那么就会立马执行 SQL 语句。需要注意的是,做切片后不能再执行filter
方法,否则会报错。调用
len
函数:调用len
函数用来获取QuerySet
中总共有多少条数据也会执行 SQL 语句。调用
list
函数:调用list
函数用来将一个QuerySet
对象转换为list
对象也会立马执行SQL
语句。判断:如果对某个
QuerySet
进行判断,也会立马执行SQL
语句。
limit 21问题:理解Django的运行机制
# 代码
result = Book.objects.annotate(total=Sum("bookorder__price")).values("name", "total")
print(result)
print(result.query)
print(connection.queries[-1]['sql'])
# print(result.query) 的打印结果
"""
SELECT front_book.name, SUM(front_book_order.price) AS total
FROM front_book
LEFT OUTER JOIN front_book_order
ON (front_book.id = front_book_order.book_id)
GROUP BY front_book.id
ORDER BY NULL
"""
# print(connection.queries[-1]['sql']) 的打印结果
"""
SELECT front_book.name, SUM(front_book_order.price) AS total
FROM front_book
LEFT OUTER JOIN front_book_order
ON (front_book.id = front_book_order.book_id)
GROUP BY front_book.id
ORDER BY NULL
LIMIT 21
"""
为什么使用connection会多一个limit 21?
在开发环境中(如
DEBUG=True
),Django 会对查询结果自动添加LIMIT 21
,这是为了防止意外加载大量数据导致性能问题。LIMIT 21
中的21
是一个经验值,足够显示部分数据(如管理后台或 Shell 中的预览),同时避免全表扫描。
connection.queries
记录实际执行的 SQL。当你在 Shell 中打印result
(如print(result)
)时,Django 会实际执行查询并获取前 21 条记录(通过LIMIT 21
),因此日志中会记录这个限制。为什么在执行
print(result)
的时候会有这个限制? 因为当你写print(result)
的时候,其实你你并没有显示地说要“遍历所有数据”,只是想“看一看 result 是啥”。Django 的开发者们非常体贴地想:“你大概只想看看前几条数据来验证查询结构对不对,不是真要拉全表数据吧?那我就默认拿 21 条给你看看。” 于是 Django 在QuerySet.__repr__()
方法内部做了这样一件事(你可以去源码验证):list(self[:REPR_OUTPUT_SIZE + 1]) # 默认 REPR_OUTPUT_SIZE = 20
但是生产环境(
DEBUG=False
) 时无此限制。
什么时候会全量查询?
Django 的查询集不会立即执行数据库查询,只有在以下操作发生时才会真正触发 SQL 执行
- 遍历查询集(
for item in queryset
)- 索引/切片访问(
queryset[0]
或queryset[5:10]
)- 转换为列表/字典(
list(queryset)
或dict(queryset.values_list())
)- 调用聚合函数(
queryset.count()
,queryset.exists()
)- 序列化/打印(
print(queryset)
或json.dumps(queryset)
)如果只是定义查询集(如
books = Book.objects.all()
),不会 触发 SQL 查询。
ORM模型与表的同步
迁移命令
makemigrations
makemigrations
:将模型生成迁移脚本。模型所在的 app
,必须放在 settings.py
中的INSTALLED_APPS
中。
这个命令有以下几个常用选项:
app_label
:后面可以跟一个或者多个app
,那么就只会针对这几个app
生成迁移脚本。如果没有任何的app_label
,那么会检查INSTALLED_APPS
中所有的app
下的模型,针对每一个app
都生成响应的迁移脚本。name
:给这个迁移脚本指定一个名字。empty
:生成一个空的迁移脚本。如果你想写自己的迁移脚本,可以使用这个命令来实现一个空的文件,然后自己再在文件中写迁移脚本。
migrate
migrate
:将新生成的迁移脚本映射到数据库中。创建新的表或者修改表的结构。
以下一些常用的选项:
app_label
:将某个app
下的迁移脚本映射到数据库中。如果没有指定,那么会将所有在INSTALLED_APPS
中的app
下的模型都映射到数据库中。app_label migrationname
:将某个app
下指定名字的migration
文件映射到数据库中。fake
:可以将指定的迁移脚本名字添加到数据库中。但是并不会把迁移脚本转换为SQL
语句,修改数据库中的表。fake-initial
:将第一次生成的迁移文件版本号记录在数据库中。但并不会真正的执行迁移脚本。
showmigrations
showmigrations
:查看某个app
下的迁移文件。如果后面没有app
,那么将查看 INSTALLED_APPS
中所有的迁移文件。
sqlmigrate
sqlmigrate
:查看某个迁移文件在映射到数据库中的时候,转换的 SQL 语句。
迁移版本对不上
migrations
中的迁移版本和数据库中的迁移版本对不上怎么办?
找到哪里不一致,然后使用
python manage.py --fake [版本名字]
,将这个版本标记为已经映射。删除指定
app
下migrations
和数据库表django_migrations
中和这个app
相关的版本号,然后将模型中的字段和数据库中的字段保持一致,再使用命令python manage.py makemigrations
重新生成一个初始化的迁移脚本,之后再使用命令python manage.py makemigrations --fake initial
来将这个初始化的迁移脚本标记为已经映射。以后再修改就没有问题了。
更多关于迁移脚本的。请查看官方文档:https://docs.djangoproject.com/en/5.0/topics/migrations/
根据已有的表自动生成模型
在实际开发中,有些时候可能数据库已经存在了。如果我们用 Django
来开发一个网站,读取的是之前已经存在的数据库中的数据。那么该如何将模型与数据库中的表映射呢?根据旧的数据库生成对应的 ORM
模型,需要以下几个步骤:
1-生成模型
Django
给我们提供了一个 inspectdb
的命令,可以非常方便的将已经存在的表,自动的生成模型。想要使用 inspectdb
自动将表生成模型。首先需要在 settings.py
中配置好数据库相关信息。不然就找不到数据库。示例代码如下:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': "migrations_demo",
'HOST': '127.0.0.1',
'PORT': '3306',
'USER': 'root',
'PASSWORD': 'root'
}
}
然后通过 python manage.py inspectdb
,就会将表转换为模型后的代码显示在终端:
from django.db import models
class ArticleArticle(models.Model):
title = models.CharField(max_length=100)
content = models.TextField(blank=True, null=True)
create_time = models.DateTimeField(blank=True, null=True)
author = models.ForeignKey('FrontUserFrontuser', models.DO_NOTHING, blank=True, null=True)
class Meta:
managed = False
db_table = 'article_article'
class ArticleArticleTags(models.Model):
article = models.ForeignKey(ArticleArticle, models.DO_NOTHING)
tag = models.ForeignKey('ArticleTag', models.DO_NOTHING)
class Meta:
managed = False
db_table = 'article_article_tags'
unique_together = (('article', 'tag'),)
class ArticleTag(models.Model):
name = models.CharField(max_length=100)
class Meta:
managed = False
db_table = 'article_tag'
class FrontUserFrontuser(models.Model):
username = models.CharField(max_length=100)
telephone = models.CharField(max_length=11)
class Meta:
managed = False
db_table = 'front_user_frontuser'
以上代码只是显示在终端。如果想要保存到文件中。那么可以使用 >
重定向输出到指定的文件。比如让他输出到 models.py
文件中。示例命令如下:
python manage.py inspectdb > models.py
以上的命令,只能在终端执行,不能在 pycharm->Tools->Run manage.py Task...
中使用。
如果只是想要转换一个表为模型。那么可以指定表的名字。示例命令如下:
python manage.py inspectdb article_article > models.py
2-修正模型
新生成的 ORM 模型有些地方可能不太适合使用。比如模型的名字,表之间的关系等等。那么以下选项还需要重新配置一下:
模型名:自动生成的模型,是根据表的名字生成的,可能不是你想要的。这时候模型的名字你可以改成任何你想要的。
模型所属app:根据自己的需要,将相应的模型放在对应的app中。放在同一个app中也是没有任何问题的。只是不方便管理。
模型外键引用:将所有使用
ForeignKey
的地方,模型引用都改成字符串。这样不会产生模型顺序的问题。另外,如果引用的模型已经移动到其他的app中了,那么还要加上这个app的前缀。让Django管理模型:将
Meta
下的managed=False
删掉,如果保留这个,那么以后这个模型有任何的修改,使用migrate
都不会映射到数据库中。当有多对多的时候,应该也要修正模型。将中间表注释了,然后使用
ManyToManyField
来实现多对多。并且,使用ManyToManyField
生成的中间表的名字可能和数据库中那个中间表的名字不一致,这时候肯定就不能正常连接了。那么可以通过db_table
来指定中间表的名字。示例代码如下:class Article(models.Model): title = models.CharField(max_length=100, blank=True, null=True) content = models.TextField(blank=True, null=True) author = models.ForeignKey('front.User', models.SET_NULL, blank=True, null=True) # 使用ManyToManyField模型到表,生成的中间表的规则是:article_tags # 但现在已经存在的表的名字叫做:article_tag # 可以使用db_table,指定中间表的名字 tags = models.ManyToManyField("Tag",db_table='article_tag') class Meta: db_table = 'article'
表名:切记不要修改表的名字。不然映射到数据库中,会发生找不到对应表的错误
3-生成迁移脚本
执行命令 python manage.py makemigrations
生成初始化的迁移脚本。方便后面通过 ORM 来管理表。这时候还需要执行命令 python manage.py migrate --fake-initial
,因为如果不使用 --fake-initial
,那么会将迁移脚本会映射到数据库中。这时候迁移脚本会新创建表,而这个表之前是已经存在了的,所以肯定会报错。此时我们只要将这个 0001-initial
的状态修改为已经映射,而不真正执行映射,下次再 migrate
的时候,就会忽略他。
4-将核心表映射到数据库中
将 Django 的核心表映射到数据库中: Django 中还有一些核心的表也是需要创建的。不然有些功能是用不了的。比如 auth
相关表。如果这个数据库之前就是使用 Django 开发的,那么这些表就已经存在了。可以不用管了。如果之前这个数据库不是使用 Django
开发的,那么应该使用 migrate
命令将 Django 中的核心模型映射到数据库中。
回退迁移
回退迁移
# 基本语法 python manage.py migrate <app_name> <migration_name> # 我想回退到0006_alter_article_author.py 的状态 python manage.py migrate article 0006 Operations to perform: Target specific migration: 0006_alter_article_author, from article Running migrations: No migrations to apply.
删除错误的迁移文件:
0007
,0008
,0009
清空数据库中对应的表(可选)
重新生成并应用迁移
python manage.py makemigrations python manage.py migrate
对已有表格新增外键
# Article 是之前的模型,并且已经在数据库,且相关表格已有数据
class Article(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
author = models.ForeignKey("User", on_delete=models.PROTECT,related_name="fff",related_query_name='aaa')
tags = models.ManyToManyField("Tag", related_name="articles")
# category 是新增的外键,这个时候要设置 null=True,这会允许 category 字段暂时为 NULL。
# 如果没有配置null=True, 在迁移脚本的时候会报错
category = models.ForeignKey("Category", on_delete=models.CASCADE, null=True)
# 这是新增的表
class Category(models.Model):
"""文章分类表"""
name = models.CharField(max_length=100)
高级视图 todo
Django限制请求method
常用的请求method
GET请求:GET请求一般用来向服务器索取数据,但不会向服务器提交数据,不会对服务器的状态进行更改。比如向服务器获取某篇文章的详情。
POST请求:POST请求一般是用来向服务器提交数据,会对服务器的状态进行更改。比如提交一篇文章给服务器。
限制请求装饰器
Django
内置的视图装饰器可以给视图提供一些限制。比如这个视图只能通过 GET
的 method
访问等。以下将介绍一些常用的内置视图装饰器。
django.http.decorators.http.require_http_methods
:这个装饰器需要传递一个允许访问的方法的列表。比如只能通过GET
的方式访问。那么示例代码如下from django.views.decorators.http import require_http_methods @require_http_methods(["GET"]) def my_view(request): pass
django.views.decorators.http.require_GET
:这个装饰器相当于是require_http_methods(['GET'])
的简写形式,只允许使用GET
的 method 来访问视图。示例代码如下:from django.views.decorators.http import require_GET @require_GET def my_view(request): pass
django.views.decorators.http.require_POST
:这个装饰器相当于是require_http_methods(['POST'])
的简写形式,只允许使用POST
的 method 来访问视图。示例代码如下:from django.views.decorators.http import require_POST @require_POST def my_view(request): pass
django.views.decorators.http.require_safe
:这个装饰器相当于是require_http_methods(['GET','HEAD'])
的简写形式,只允许使用相对安全的方式来访问视图。因为GET
和HEAD
不会对服务器产生增删改的行为。因此是一种相对安全的请求方式。示例代码如下:from django.views.decorators.http import require_safe @require_safe def my_view(request): pass
页面重定向
重定向分为永久性重定向和暂时性重定向,在页面上体现的操作就是浏览器会从一个页面自动跳转到另外一个页面。比如用户访问了一个需要权限的页面,但是该用户当前并没有登录,因此我们应该给他重定向到登录页面
- 永久性重定向:http的状态码是
301
,多用于旧网址被废弃了要转到一个新的网址确保用户的访问,最经典的就是京东网站,你输入www.jingdong.com
的时候,会被重定向到www.jd.com
,因为jingdong.com
这个网址已经被废弃了,被改成jd.com
,所以这种情况下应该用永久重定向。 - 暂时性重定向:http的状态码是
302
,表示页面的暂时性跳转。比如访问一个需要权限的网址,如果当前用户没有登录,应该重定向到登录页面,这种情况下,应该用暂时性重定向。
在 Django 中,重定向是使用 redirect(to, *args, permanent=False, **kwargs)
来实现的。 to
是一个 url
, permanent
代表的是这个重定向是否是一个永久的重定向,默认是 False
。关于重定向的使用,请看以下例子:
from django.shortcuts import reverse,redirect
def profile(request):
if request.GET.get("username"):
return HttpResponse("%s,欢迎来到个人中心页面!")
else:
return redirect(reverse("user:login"))
def to_profile(request):
return redirect('/profile/') # 默认 302
def to_new_site(request):
return redirect('/new-site/', permanent=True) # 永久重定向 301
项 | 301 永久重定向 | 302 暂时重定向 |
---|---|---|
地址栏跳转后地址 | /new |
/new |
地址栏显示方式 | 一样 | 一样 |
用户能不能看出来? | 肉眼几乎看不出 | 一样几乎看不出 |
浏览器行为(背后) | 会缓存重定向 | 不会缓存重定向 |
搜索引擎处理 | 会替换为新地址 | 不会替换旧地址 |
场景 | 建议使用状态码 |
---|---|
页面临时维护或跳转 | 302(暂时) |
登录后跳转 | 302(暂时) |
域名更换、旧链接废弃 | 301(永久) |
网站改版、URL 重构 | 301(永久) |
表单提交后跳转 | 302(暂时) |
SEO 优化,替换老页面 | 301(永久) |
redirect
和render
的区别
主要区别
特性 | redirect |
render |
---|---|---|
HTTP 状态码 | 302/301 | 200 |
浏览器行为 | 发起新请求,URL 改变 | 直接显示内容,URL 不变 |
数据传递 | 需通过 URL 或 Session 传递 | 直接通过 context 传递 |
模板渲染 | 不渲染,跳转到新视图处理 | 立即渲染指定模板 |
典型用途 | 跳转场景(如 POST 后的 GET) | 显示动态内容(如详情页) |
如何选择
- 用
redirect
当:- 需要 改变 URL(如提交表单后防重复提交)。
- 需要 触发新请求(如登录后刷新权限状态)。
- 用
render
当:- 需要 直接显示页面(如加载详情页)。
- 需要 保留当前 URL(如表单验证错误重新渲染表单页)。
常见误区
render
不会改变 URL
即使渲染了另一个模板,浏览器地址栏仍显示原 URL(用户不知道背后发生了什么)。redirect
会丢失 POST 数据
如果需要保留数据,需存入 Session 或通过 URL 参数传递。- 性能影响
redirect
比render
多一次 HTTP 请求,但能避免重复提交等问题。
WSGIRequest对象
Django
在接收到http
请求之后,会根据http
请求携带的参数以及报文信息创建一个 WSGIRequest
对象,并且作为视图函数第一个参数传给视图函数。也就是我们经常看到的 request
参数。在这个对象上我们可以找到客户端上传上来的所有信息。这个对象的完整路径是django.core.handlers.wsgi.WSGIRequest
。
WSGIRequest对象常用属性
WSGIRequest
对象上大部分的属性都是只读的。因为这些属性是从客户端上传上来的,没必要做任何的修改。以下将对一些常用的属性进行讲解:
path
:请求服务器的完整“路径”,但不包含域名和参数。比如http://www.baidu.com/xxx/yyy/
,那么path
就是/xxx/yyy/
。method
:代表当前请求的 http 方法。比如是GET
还是POST
。GET
:一个django.http.request.QueryDict
对象。操作起来类似于字典。这个属性中包含了所有以?xxx=xxx
的方式上传上来的参数。POST
:也是一个django.http.request.QueryDict
对象。这个属性中包含了所有以POST
方式上传上来的参数。FILES
:也是一个django.http.request.QueryDict
对象。这个属性中包含了所有上传的文件。COOKIES
:一个标准的Python字典,包含所有的 cookie ,键值对都是字符串类型。session
:一个类似于字典的对象。用来操作服务器的session
。META
:存储的客户端发送上来的所有header
信息。CONTENT_LENGTH
:请求的正文的长度(是一个字符串)。CONTENT_TYPE
:请求的正文的MIME类型。HTTP_ACCEPT
:响应可接收的Content-Type
。HTTP_ACCEPT_ENCODING
:响应可接收的编码。HTTP_ACCEPT_LANGUAGE
: 响应可接收的语言。HTTP_HOST
:客户端发送的HOST值。HTTP_REFERER
:在访问这个页面上一个页面的url。QUERY_STRING
:单个字符串形式的查询字符串(未解析过的形式)。REMOTE_ADDR
:客户端的IP
地址。如果服务器使用了nginx
做反向代理或者负载均衡,那么这个值返回的是127.0.0.1
,这时候可以使用HTTP_X_FORWARDED_FOR
来获取,所以获取ip
地址的代码片段如下:if request.META.has_key('HTTP_X_FORWARDED_FOR'): ip = request.META['HTTP_X_FORWARDED_FOR'] else: ip = request.META['REMOTE_ADDR']
REMOTE_HOST
:客户端的主机名。REQUEST_METHOD
:请求方法。一个字符串类似于 GET 或者 POST 。SERVER_NAME
:服务器域名。SERVER_PORT
:服务器端口号,是一个字符串类型。
WSGIRequest对象常用方法
is_secure()
:是否是采用 https 协议。get_host()
:服务器的域名。如果在访问的时候还有端口号,那么会加上端口号。比如www.baidu.com:9000
。get_full_path()
:返回完整的path。如果有查询字符串,还会加上查询字符串。比如/music/bands/?print=True
。get_raw_uri()
:获取请求的完整 url 。
QueryDict对象
我们平时用的 request.GET
和 request.POST
都是 QueryDict
对象,这个对象继承自 dict
,因此用法跟 dict
相差无几。其中用得比较多的是 get
方法和 getlist
方法
get
方法:用来获取指定 key 的值,如果没有这个 key ,那么会返回 None 。getlist
方法:如果浏览器上传上来的 key 对应的值有多个,那么就需要通过这个方法获取。
HttpResponse对象
Django
服务器接收到客户端发送过来的请求后,会将提交上来的这些数据封装成一个 HttpRequest
对象传给视图函数。那么视图函数在处理完相关的逻辑后,也需要返回一个响应给浏览器。而这个响应,我们必须返回 HttpResponseBase
或者他的子类的对象。而 HttpResponse
则是 HttpResponseBase
用得最多的子类。那么接下来就来介绍一下 HttpResponse
及其子类。
常用属性
content
:返回的内容。
status_code
:返回的HTTP响应状态码。
content_type
:返回的数据的MIME类型,默认为 text/html
。浏览器会根据这个属性,来显示数据。如果是 text/html
,那么就会解析这个字符串,如果 text/plain
,那么就会显示一个纯文本。常用的 Content-Type
如下:
text/html
(默认的,html文件)text/plain
(纯文本)text/css
(css文件)text/javascript
(js文件)multipart/form-data
(文件提交)application/json
(json传输)application/xml
(xml文件)
- 设置请求头:
response['X-Access-Token'] = 'xxxx'
。
常用方法
set_cookie
:用来设置 cookie
信息。后面讲到授权的时候会着重讲到。
delete_cookie
:用来删除 cookie
信息。
write
: HttpResponse
是一个类似于文件的对象,可以用来写入数据到数据体(content
)中。
JsonResponse类
用来对象 dump
成 json
字符串,然后返回将 json
字符串封装成 Response
对象返回给浏览器。并且他的 Content-Type
是 application/json
。示例代码如下:
from django.http import JsonResponse
def index(request):
return JsonResponse({"username":"zhiliao","age":18})
默认情况下 JsonResponse
只能对字典进行 dump
,如果想要对非字典的数据进行 dump
,那么需要给JsonResponse
传递一个 safe=False
参数。示例代码如下:
from django.http import JsonResponse
def index(request):
persons = ['张三','李四','王五']
return HttpResponse(persons)
以上代码会报错,应该在使用 HttpResponse
的时候,传入一个 safe=False
参数,示例代码如下:
return HttpResponse(persons,safe=False)
生成CSV文件
有时候我们做的网站,需要将一些数据,生成有一个 CSV 文件给浏览器,并且是作为附件的形式下载下来。以下将讲解如何生成 CSV 文件。
生成小的CSV文件
我们用 Python
内置的csv
模块来处理 csv
文件,并且使用 HttpResponse
来将 csv
文件返回回去。示例代码如下:
import csv
from django.http import HttpResponse
def csv_view(request):
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
writer = csv.writer(response)
writer.writerow(['username', 'age', 'height', 'weight'])
writer.writerow(['zhiliao', '18', '180', '110'])
return response
- 我们在初始化
HttpResponse
的时候,指定了Content-Type
为text/csv
,这将告诉浏览器,这是一个csv
格式的文件而不是一个HTML
格式的文件,如果用默认值,默认值就是html
,那么浏览器将把csv
格式的文件按照html
格式输出,这肯定不是我们想要的。 - 第二个我们还在
response
中添加一个Content-Disposition
头,这个东西是用来告诉浏览器该如何处理这个文件,我们给这个头的值设置为attachment
; 那么浏览器将不会对这个文件进行显示,而是作为附件的形式下载,第二个filename="somefilename.csv"
是用来指定这个 csv 文件的名字。 - 我们使用
csv
模块的writer
方法,将相应的数据写入到response
中。
将csv文件定义成模板
我们还可以将 csv
格式的文件定义成模板,然后使用 Django
内置的模板系统,并给这个模板传入一个Context
对象,这样模板系统就会根据传入的 Context
对象,生成具体的 csv
文件。示例代码如下:
模板文件:
{% for row in data %}
"{{ row.0|addslashes }}", "{{ row.1|addslashes }}", "{{ row.2|addslashes }}", "{{ row.3|addslashes }}", "{{ row.4|addslashes }}"
{% endfor %}
这其实是一个 Django 模板(template)文件,它会被用来动态生成 CSV 文件的内容,其中的 {% for %}
和 {{ ... }}
是 Django 的模板语言语法。
上面的意思是:对 data
中的每一行 row
,输出一行 CSV 内容,每个字段用逗号分隔,并且加上引号,字段中的特殊字符用 addslashes
过滤(防止引号、反斜杠出错)。
row
是一个列表或元组,.0
表示取第 0 个元素,.1
是第 1 个,以此类推。
举个例子:
data = [
["Python入门", "张三", "978-7-123456-78-9", 88.00, "2024-01-01"],
["Django实战", "李四", "978-7-987654-32-1", 108.00, "2024-02-01"],
]
模板渲染后就是:
"Python入门", "张三", "978-7-123456-78-9", "88.0", "2024-01-01"
"Django实战", "李四", "978-7-987654-32-1", "108.0", "2024-02-01"
addslashes
是一个模板过滤器,用于给字符串中的特殊字符 自动加反斜杠(\
)转义,防止格式出错或被解释错
字符 | 转义后 |
---|---|
' |
\' |
" |
\" |
\ |
\\ |
为什么需要转义?
- 如果原始字符串是:
He said, "Hello"
- 如果不加处理,CSV 会长这样:
"He said, "Hello""
- 这样会让解析 CSV 的程序或 Excel 报错。因为CSV 格式规定,字段要用双引号包起来
- 但是加了
addslashes
就会变成:"He said, \"Hello\""
视图函数:
from django.http import HttpResponse
from django.template import loader, Context
def cvs1(request):
# 指定了 `Content-Type` 为 `text/csv` ,
# 这将告诉浏览器,这是一个 `csv `格式的文件而不是一个 `HTML` 格式的文件
response = HttpResponse(content_type='text/csv')
# 在 `response` 中添加一个 `Content-Disposition` 头
# 我们给这个头的值设置为 `attachment`; 那么浏览器将不会对这个文件进行显示,而是作为附件的形式下载
# 第二个 `filename="somefilename.csv"` 是用来指定这个 csv 文件的名字 为 somefilename.csv
response['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
csv_data = (
('First row', 'Foo', 'Bar', 'Baz'),
('Second row', 'A', 'B', 'C', '"Testing"', "Here's a quote"),
)
# 用 loader.get_template() 加载了模板
t = loader.get_template('my_template_name.txt')
'''
render() 方法会把 t 这个模板和你传入的上下文变量 {"data": csv_data} 结合,生成一段最终的字符串(通常是 HTML 或 CSV)。
{"data": csv_data} 是上下文字典: 把 csv_data 作为 data 变量传给模板
response.write(...): 这是 Django 的 HttpResponse 对象的 write() 方法,用于往响应中写入内容。
- 调用 response.write(...),就是往这个响应写入 CSV 内容
'''
response.write(t.render({"data": csv_data}))
return response
其中模版就是放在my_template_name.txt
里面:
生成大的CSV文件:
以上的例子是生成的一个小的 csv 文件,如果想要生成大型的 csv 文件,那么以上方式将有可能会发生超时的情况(服务器要生成一个大型csv文件,需要的时间可能会超过浏览器默认的超时时间)。这时候我们可以借助另外一个类,叫做 StreamingHttpResponse
对象,这个对象是将响应的数据作为一个流返回给客户端,而不是作为一个整体返回。示例代码如下:
class Echo:
"""
定义一个可以执行写操作的类,以后调用 csv.writer 的时候,就会执行这个 write 方法。
它模拟了一个具有 write 方法的伪文件对象,csv.writer 依赖这个接口。
这里返回值直接是传入的 value,目的是让数据一行行地传回给 StreamingHttpResponse。
"""
def write(self, value):
return value
def cvs2(request):
"""
rows 是一个生成器,动态生成 655,360 行数据,每行是一个列表:比如 ["Row 123", "123"]。
使用生成器节省内存,不会一次性把所有行加载进内存。
"""
rows = (["Row {}".format(idx), str(idx)] for idx in range(655360))
# pseudo_buffer 是你模拟的输出对象。
pseudo_buffer = Echo()
# writer 是一个标准的 CSV 写入器,它会将每一行格式化为 CSV 格式的字符串,然后调用 pseudo_buffer.write() 返回字符串。
writer = csv.writer(pseudo_buffer)
"""
创建 StreamingHttpResponse
注意这里的 (writer.writerow(row) for row in rows) 是一个生成器表达式。
每写一行 row,writer.writerow 会返回一个字符串(通过 Echo.write()),该字符串直接被写入响应。
"""
response = StreamingHttpResponse(
(writer.writerow(row) for row in rows),
content_type="text/csv"
)
response['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
return response
生成器表达式
rows = (["Row {}".format(idx), str(idx)] for idx in range(655360))
for idx in range(655360)
:range(655360)
会生成一个从0
到655359
的整数序列,总共有 655,360 个数字。idx
每次循环就是当前的数字,比如第一次是0
,第二次是1
,以此类推。
["Row {}".format(idx), str(idx)]
: 是创建了一个列表,里面有两个元素,每次根据idx
不同,生成不同的内容"Row {}".format(idx)
: 把数字idx
插入到字符串"Row {}"
里面,生成像"Row 0"
,"Row 1"
,"Row 2"
这样的字符串。str(idx)
: 把数字idx
转成字符串,比如 0 变成"0"
,1 变成"1"
。- 比如当
idx = 5
的时候,["Row {}".format(5), str(5)]
就变成了["Row 5", "5"]
- 类似的表达式还可以写成:
[idx, idx * idx]
。当idx = 4
时,结果是[4, 16]
整个外面
( ... for idx in range(655360))
是一个生成器表达式- 生成器表达式不会一次性生成所有的行,而是在需要的时候才动态创建一行数据(节省内存)。
rows
是一个生成器对象,你可以用for
来遍历它。
列表生成式和生成器表达式
列表生成式(全部生成在内存里)
# 立即占内存,5个元素都生成好了。
list_rows = [["Row {}".format(idx), str(idx)] for idx in range(5)]
# list_rows 的内容
[
['Row 0', '0'],
['Row 1', '1'],
['Row 2', '2'],
['Row 3', '3'],
['Row 4', '4'],
]
生成器表达式(只有用到的时候才生成)
# 只是定义了生成器对象,啥数据都没生成
rows = (["Row {}".format(idx), str(idx)] for idx in range(5))
# rows 是一个生成器对象
for row in rows: # # 生成器开始真正生成第一个数据
print(row) # 执行完之后,再次进入循环,再生成一行新的数据
"""
输出是:
['Row 0', '0']
['Row 1', '1']
['Row 2', '2']
['Row 3', '3']
['Row 4', '4']
"""
项目 | 列表生成式 [...] |
生成器表达式 (...) |
---|---|---|
存储 | 一次性生成完整的列表(占内存) | 每次按需生成一个元素(节省内存) |
返回值 | 返回一个列表对象 | 返回一个生成器对象 |
使用场景 | 数据量小,可以全部放进内存 | 数据量大,希望一边算一边用 |
语法 | [表达式 for 变量 in 可迭代对象] |
(表达式 for 变量 in 可迭代对象) |
Echo的作用
首先,csv.writer
的官方要求是:
csv.writer(file_like_object)
需要传入一个“有.write(str)
方法的对象”。
也就是说,csv.writer
并不在乎你传进来的是不是一个真正的文件,它只关心:
- 能不能
.write()
一个字符串。
而你这里的 Echo
:
class Echo:
def write(self, value):
return value
实现了 .write(value)
方法,符合 csv.writer
需要的接口!
但是它的 write
并没有真正写到文件或者磁盘上,而是直接把 value
返回了!
为什么要这样做?
- 正常来说,
csv.writer
会把格式化好的 CSV 行写到一个文件对象里。 - 但是在你的程序中,并不想真正地保存成文件,而是想把 CSV 数据一行行地流式发给前端浏览器。
- 所以,这里用
Echo
来「骗」一下csv.writer
,让它生成一行字符串,而不是实际写文件。 - 然后,每生成一行字符串,就通过
StreamingHttpResponse
直接发送给浏览器。
总结一句话:
Echo 是一个伪文件对象。
它的 write 方法让 csv.writer 把每行 CSV 格式的数据「写出来」——实际上是直接返回了字符串。
最后用 StreamingHttpResponse 按行流式发送给前端。
write的具体过程
class Echo:
def write(self, value):
return value
pseudo_buffer = Echo()
writer = csv.writer(pseudo_buffer)
response = StreamingHttpResponse(
(writer.writerow(row) for row in rows),
content_type="text/csv"
)
pseudo_buffer = Echo()
: 创建了一个 Echo 类的实例writer = csv.writer(pseudo_buffer)
: 创建了一个 CSV写手(writer对象)。csv.writer(file_like_object)
要求你传一个有.write()
方法的对象。pseudo_buffer
(Echo实例)刚好有write()
,所以符合要求。
response = StreamingHttpResponse(...)
: 创建了一个 流式响应对象,能一点点地把数据发给浏览器,而不是一次性生成好大文件。writer.writerow(row)
:- 把
row
(比如["Row 5", "5"]
)格式化成一行 CSV 字符串,比如Row 5,5\r\n
- 把这个格式化好的字符串,传给你提供的
pseudo_buffer.write()
方法
- 把
pseudo_buffer.write()
:pseudo_buffer.write(value)
收到数据,直接返回了格式化好的字符串。- 这个字符串被
StreamingHttpResponse
拿去发送到前端。
关于StreamingHttpResponse
这个类是专门用来处理流数据的。使得在处理一些大型文件的时候,不会因为服务器处理时间过长而到时连接超时。这个类不是继承自 HttpResponse
,并且跟 HttpResponse
对比有以下几点区别:
- 这个类没有属性
content
,相反是streaming_content
。 - 这个类的
streaming_content
必须是一个可以迭代的对象。 - 这个类没有
write
方法,如果给这个类的对象写入数据将会报错。
注意: StreamingHttpResponse
会启动一个进程来和客户端保持长连接,所以会很消耗资源。所以如果不是特殊要求,尽量少用这种方法。
类视图: todo
在写视图的时候, Django 除了使用函数作为视图,也可以使用类作为视图。使用类视图可以使用类的一些特性,比如继承等。
View
**django.views.generic.base.View
**是主要的类视图,所有的类视图都是继承自他。
如果我们写自己的类视图,也可以继承自他,然后再根据当前请求的 method
,来实现不同的方法。
比如这个视图只能使用get
的方式来请求,那么就可以在这个类中定义 get(self,request,*args,**kwargs)
方法。以此类推,如果只需要实现 post
方法,那么就只需要在类中实现 post(self,request,*args,**kwargs)
。示例代码如下:
# Path 路径上没有参数
from django.views import View
class BookDetailView(View): # 需要继承 View
def get(self,request,*args,**kwargs):
return render(request,'detail.html')
# path 路径上有参数:detail/<book_id>
class BookDetailView(View):
def get(self, request, book_id, *args, **kwargs):
content={"book_id":book_id}
return render(request, 'detail.html',context=content)
类视图写完后,还应该在 urls.py
中进行映射,映射的时候就需要调用 View
的类方法 as_view()
来进行转换。示例代码如下:
urlpatterns = [
path("detail/<book_id>",views.BookDetailView.as_view(),name='detail')
]
这里面的views
是指views.py
文件,BookDetailView
指views.py
里面定义的类
除了 get 方法, View 还支持['get','post','put','patch','delete','head','options','trace']
如果用户访问了 View
中没有定义的方法。比如你的类视图只支持 get
方法,而出现了 post
方法,那么就会把这个请求转发给 http_method_not_allowed(request,*args,**kwargs)
。示例代码如下:
class AddBookView(View):
def post(self,request,*args,**kwargs):
return HttpResponse("书籍添加成功!")
def http_method_not_allowed(self, request, *args, **kwargs):
return HttpResponse("您当前采用的method是:%s,本视图只支持使用post请求!" % request.method)
urls.py
中的映射如下:
path("addbook/",views.AddBookView.as_view(),name='add_book')
如果你在浏览器中访问 addbook/
,因为浏览器访问采用的是 get
方法,而 addbook
只支持 post
方法,
因此以上视图会返回: 您当前采用的 method 是: GET ,本视图只支持使用 post 请求!
。
其实不管是 get 请求还是 post 请求,都会走 dispatch(request,*args,**kwargs)
方法,所以如果实现这个方法,将能够对所有请求都处理到。
TemplateView
django.views.generic.base.TemplateView
,这个类视图是专门用来返回模版的。在这个类中,有两个属性是经常需要用到的,一个是 template_name
,这个属性是用来存储模版的路径, TemplateView
会自动的渲染这个变量指向的模版。另外一个是 get_context_data
,这个方法是用来返回上下文数据的,也就是在给模版传的参数的。示例代码如下:
from django.views.generic.base import TemplateView
class HomePageView(TemplateView): # 需要继承 TemplateView
template_name = "home.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['username'] = "令狐冲"
return context
在 urls.py
中的映射代码如下:
from django.views.generic.base import TemplateView
class HomePageView(TemplateView): # 需要继承 TemplateView
template_name = "home.html"
def get_context_data(self, **kwargs):
# context 是一个 Python 字典(dict)类型
context = super().get_context_data(**kwargs)
context['username'] = "令狐冲"
return context
如果在模版中不需要传递任何参数,那么可以直接只在 urls.py
中使用 TemplateView
来渲染模版。示例代码如下:
from django.urls import path
from django.views.generic import TemplateView
urlpatterns = [
path('about/', TemplateView.as_view(template_name="about.html")),
]
ListView
在网站开发中,经常会出现需要列出某个表中的一些数据作为列表展示出来。比如文章列表,图书列表等等。在 Django 中可以使用 ListView 来帮我们快速实现这种需求。
views.py
的示例代码如下:
from django.views.generic import ListView
class BookListView(ListView):
model = Book
template_name = 'book_list.html'
paginate_by = 10
context_object_name = 'books'
ordering = 'create_time'
page_kwarg = 'page'
def get_context_data(self, **kwargs):
# 先执行
print("-----------1-get_context_data------------")
context = super(BookListView, self).get_context_data(**kwargs)
print(context)
return context
def get_queryset(self):
# 后执行
print("-----------2-get_queryset------------")
return Book.objects.filter(id__lte=3)
对以上代码进行解释:
- 首先
ArticleListView
是继承自ListView
。 model
:重写model
类属性,指定这个列表是给哪个模型的。template_name
:指定这个列表的模板。paginate_by
:指定这个列表一页中展示多少条数据。context_object_name
:指定这个列表模型在模板中的参数名称。ordering
:指定这个列表的排序方式。page_kwarg
:获取第几页的数据的参数名称。默认是 page 。get_context_data
:获取上下文的数据。get_queryset
:如果你提取数据的时候,并不是要把所有数据都返回,那么你可以重写这个方法。将一些不需要展示的数据给过滤掉。
book_list.html
的示例代码如下:
<table>
<thead>
<tr>
<th>书籍</th>
<th>价格</th>
<th>评分</th>
<th>作者</th>
</tr>
</thead>
<tbody>
{% for book in books %}
<tr>
<td>{{book.name}}</td>
<td>{{book.price}}</td>
<td>{{book.rating}}</td>
<td>{{book.author.name}}</td>
</tr>
{% endfor %}
</tbody>
urls.py
的代码如下
path('booklist/', BookListView.as_view(),name="booklist"),
Paginator和Page类
Paginator
和 Page
类都是用来做分页的。他们在 Django 中的路径为django.core.paginator.Paginator
和 django.core.paginator.Page
。以下对这两个类的常用属性和方法做解释:
Paginator常用属性和方法:
count
:总共有多少条数据。num_pages
:总共有多少页。page_range
:页面的区间。比如有三页,那么就 range(1,4) 。
Page常用属性和方法:
has_next
:是否还有下一页。has_previous
:是否还有上一页。next_page_number
:下一页的页码。previous_page_number
:上一页的页码。number
:当前页。start_index
:当前这一页的第一条数据的索引值。end_index
:当前这一页的最后一条数据的索引值。
给类视图添加装饰器: todo
错误处理: todo
分页: todo
表单
Django中的表单使用流程
在 Django 中的表单,主要做以下两件事:
渲染表单模板:但是一般生产上不使用。因为缺乏样式,而且一般是前端负责。
表单验证数据是否合法
使用视图类
在讲解 Django 表单的具体每部分的细节之前。我们首先先来看下整体的使用流程。这里以一个做一个留言板为例。首先我们在后台服务器定义一个表单类,继承自 django.forms.Form
。示例代码如下:
from django import forms
class MessageBoardForm(forms.Form):
title = forms.CharField(
max_length=3,
label="标题",
min_length=2,
error_messages={
"min_length": "标题字符段不符合要求!",
"max_length": "标题字符段不符合要求!",
},
)
content = forms.CharField(widget=forms.Textarea, label="内容")
email = forms.EmailField(label="邮箱")
reply = forms.BooleanField(required=False, label="回复")
然后在视图中views.py
,根据是 GET
还是 POST
请求来做相应的操作。如果是 GET
请求,那么返回一个空的表单,如果是 POST
请求,那么将提交上来的数据进行校验。示例代码如下:
from .form import MessageBoardForm
# 定义视图类:定义一个基于类的视图,继承自 Django 的 View 基类
# 这种写法比函数视图更结构化,适合处理多种 HTTP 方法
class IndexView(View):
# GET 请求处理
def get(self, request):
# 创建一个空的表单实例
form = MessageBoardForm()
# 渲染 form.html 模板,将表单对象传递给模板
# 用户会看到一个空白的表单页面
return render(request, "form.html", {"form": form})
# POST 请求处理
def post(self, request):
# 用提交的数据初始化表单 MessageBoardForm(request.POST)
form = MessageBoardForm(request.POST)
# 检查表单数据是否有效 (is_valid())
if form.is_valid():
# 如果有效:从 cleaned_data 获取清洗过的数据
title = form.cleaned_data.get("title")
content = form.cleaned_data.get("content")
email = form.cleaned_data.get("email")
reply = form.cleaned_data.get("reply")
# 返回 "success" 响应
return HttpResponse("success")
else:
# 如果无效:打印错误信息到控制台
print(form.errors)
# 返回 "fail" 响应
return HttpResponse("fail")
在使用 GET
请求的时候,我们传了一个 form
给模板,那么以后模板就可以使用 form
来生成一个表单的html
代码。在使用 POST
请求的时候,我们根据前端上传上来的数据,构建一个新的表单,这个表单是用来验证数据是否合法的,如果数据都验证通过了,那么我们可以通过 cleaned_data
来获取相应的数据。在模板中渲染表单的 HTML
代码如下:
<form action="" method="post">
{{ form }}
<td><input type="submit" value="提交"></td>
</form>
urls里的内容如下:
path("form/", IndexView.as_view(), name="from"),
实验:用get请求访问:/front/form/,得到一个空表。这个空表是根据MessageBoardForm
的定义渲染出来的。
注意,在填写好内容,点击提交前需要先注释掉settings
的csrf
的内容,如下。
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
# 'django.middleware.csrf.CsrfViewMiddleware',
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
否则报如下错误
Forbidden (CSRF cookie not set.): /front/form/
[13/May/2025 07:16:40] "POST /front/form/ HTTP/1.1" 403 2855
填好,点击提交之后,会返回success
使用视图函数
from django.views.decorators.http import require_http_methods
# 使用这个装饰器之后,就只能接收 Get和Post请求
@require_http_methods(["GET", "POST"])
def form2(request):
# 如果用GET请求,那么就直接返回一个页面
if request.method == "GET":
form = MessageBoardForm()
return render(request, "form.html", context={"form": form})
else:
# 对用post请求提交上来的数据,用表单验证是否满足要求
form = MessageBoardForm(request.POST)
if form.is_valid():
title = form.cleaned_data.get("title")
content = form.cleaned_data.get("content")
email = form.cleaned_data.get("email")
return HttpResponse(f"{title}, {content}, {email}")
else:
print(form.errors)
return HttpResponse("表单验证失败!")
表单验证
常用的Field
使用 Field 可以是对数据验证的第一步。你期望这个提交上来的数据是什么类型,那么就使用什么类型的 Field 。
CharField
用来接收文本。
参数:
max_length:这个字段值的最大长度。
min_length:这个字段值的最小长度。
required:这个字段是否是必须的。默认是必须的。
error_messages:在某个条件验证失败的时候,给出错误信息。
EmailField
用来接收邮件,会自动验证邮件是否合法。
错误信息的key: required 、 invalid 。
email = forms.EmailField(
label="邮箱",
error_messages={
"required": "邮箱地址不能为空!" # 自定义必填错误提示 (required=True,默认值)
"invalid": "请输入有效的邮箱地址!" # 自定义格式错误提示
}
)
FloatField
用来接收浮点类型,并且如果验证通过后,会将这个字段的值转换为浮点类型。
参数:
max_value:最大的值。
min_value:最小的值。
错误信息的key: required 、 invalid 、 max_value 、 min_value 。
IntegerField
用来接收整形,并且验证通过后,会将这个字段的值转换为整形。
参数:
max_value:最大的值。
min_value:最小的值。
错误信息的key: required 、 invalid 、 max_value 、 min_value 。
URLField
用来接收 url 格式的字符串。
错误信息的key: required 、 invalid 。
常用验证器
在验证某个字段的时候,可以传递一个 validators
参数用来指定验证器,进一步对数据进行过滤。验证器有很多,但是很多验证器我们其实已经通过这个 Field
或者一些参数就可以指定了。比如EmailValidator
,我们可以通过 EmailField
来指定,比如 MaxValueValidator
,我们可以通过max_value
参数来指定。以下是一些常用的验证器:
MaxValueValidator
:验证最大值。MinValueValidator
:验证最小值。MinLengthValidator
:验证最小长度。MaxLengthValidator
:验证最大长度。EmailValidator
:验证是否是邮箱格式。URLValidator
:验证是否是 URL 格式。RegexValidator
:如果还需要更加复杂的验证,那么我们可以通过正则表达式的验证器RegexValidator
。比如现在要验证手机号码是否合格,那么我们可以通过以下代码实现:# form.py from django.core import validators class MyForm(forms.Form): telephone = forms.CharField( validators=[ validators.RegexValidator( "1[345678]\d{9}", message="请输入正确格式的手机号码!" ) ] ) # views.py import json from .form import MyForm @require_http_methods(["GET", "POST"]) def myfrom(request): if request.method == "GET": return render(request, "myform.html") else: form = MyForm(request.POST) if form.is_valid(): telephone = form.cleaned_data.get("telephone") return HttpResponse(telephone) else: print(json.loads(form.errors.as_json())) return HttpResponse("表单验证失败!") # myform.html <form action="" method="POST"> <div> <input type="text" name="telephone" placeholder="请输入手机号码"> </div> <input type="submit" value="提交"> </form> # urls.py path("myform/", views.myfrom, name="myform"),
自定义验证
有时候对一个字段验证,不是一个长度,一个正则表达式能够写清楚的,还需要一些其他复杂的逻辑,那么我们可以对某个字段,进行自定义的验证。比如在注册的表单验证中,我们想要验证手机号码是否已经被注册过了,那么这时候就需要在数据库中进行判断才知道。**对某个字段进行自定义的验证方式是,定义一个方法,这个方法的名字定义规则是:clean_fieldname
。**如果验证失败,那么就抛出一个验证错误。
比如要验证用户表中手机号码之前是否在数据库中存在,那么可以通过以下代码实现:
class MyForm(forms.Form):
telephone = forms.CharField(
validators=[
validators.RegexValidator(
"1[345678]\d{9}", message="请输入正确格式的手机号码!"
)
]
)
def clean_telephone(self):
"""
self 指代当前表单实例(即 MyForm 的一个对象)。
self.cleaned_data 是 Django 表单验证后存储清洗数据的字典。
为什么用 .get() 而不是直接访问?
如果 telephone 字段未通过验证(如未填写或格式错误),cleaned_data 可能没有该键,
直接 self.cleaned_data["telephone"] 会抛出 KeyError,而 .get() 可以安全地返回 None。
"""
telephone = self.cleaned_data.get("telephone")
# 从数据库中查找telephone是否存在,如果存在,那么抛出验证错误
if telephone == "18888888888":
raise forms.ValidationError("手机号码已经存在!")
else:
return telephone # 需手动返回 cleaned_data 中的值
以上是对某个字段进行验证,如果验证数据的时候,需要针对多个字段进行验证,那么可以重写 clean
方法。比如要在注册的时候,要判断提交的两个密码是否相等。那么可以使用以下代码来完成:
# form.py
from django.core import validators
class MyForm(forms.Form):
telephone = forms.CharField(
validators=[
validators.RegexValidator(
"1[345678]\d{9}", message="请输入正确格式的手机号码!"
)
]
)
pwd1 = forms.CharField(min_length=6, max_length=100)
pwd2 = forms.CharField(min_length=6, max_length=100)
def clean_telephone(self):
"""
self 指代当前表单实例(即 MyForm 的一个对象)。
self.cleaned_data 是 Django 表单验证后存储清洗数据的字典。
为什么用 .get() 而不是直接访问?
如果 telephone 字段未通过验证(如未填写或格式错误),cleaned_data 可能没有该键,
直接 self.cleaned_data["telephone"] 会抛出 KeyError,而 .get() 可以安全地返回 None。
"""
telephone = self.cleaned_data.get("telephone")
# 从数据库中查找telephone是否存在,如果存在,那么抛出验证错误
if telephone == "18888888888":
raise forms.ValidationError("手机号码已经存在!")
else:
return telephone # 需手动返回 cleaned_data 中的值
def clean(self):
"""
cleaned_data = super().clean():
调用父类(forms.Form)的 clean() 方法,执行 Django 表单的默认验证逻辑(包括字段级别的验证,如 required、max_length 等)。
返回经过基础验证后的 cleaned_data(包含已验证的字段数据)
super() 指代当前类(MyForm)的父类(即 django.forms.Form)
为什么需要这行代码?
如果不调用 super().clean(),Django 不会执行默认的字段验证(如必填检查、长度检查等),可能导致数据不完整或无效。
"""
cleaned_data = super().clean()
pwd1 = cleaned_data.get("pwd1")
pwd2 = cleaned_data.get("pwd2")
if pwd1 != pwd2:
raise forms.ValidationError("两次密码不一致!")
else:
return cleaned_data # 需手动返回 cleaned_data 中的值
# myform.html
<body>
<form action="" method="POST">
<div>
<input type="text" name="telephone" placeholder="请输入手机号码">
</div>
<div>
<input type="password" name="pwd1" placeholder="请输入密码">
</div>
<div>
<input type="password" name="pwd2" placeholder="请重复密码">
</div>
<input type="submit" value="提交">
</form>
</body>
三个验证的顺序:
用户提交数据
│
↓
1. 字段默认验证(检查必填、格式、validators)
│
↓
2. 单字段钩子验证(clean_<fieldname>())
│
↓
3. 全局表单验证(clean())
│
↓
验证通过 → 存入 cleaned_data
验证失败 → 存入 form.errors
提取错误信息
如果验证失败了,那么有一些错误信息是我们需要传给前端的。这时候我们可以通过以下属性来获取:
form.errors
:这个属性获取的错误信息是一个包含了html
标签的错误信息。form.errors.get_json_data()
:这个方法获取到的是一个字典类型的错误信息。将某个字段的名字作为 key ,错误信息作为值的一个字典。form.errors.as_json()
:这个方法是将form.get_json_data()
返回的字典dump
成json
格式的字符串,方便进行传输。以下是错误信息{'__all__': [{'message': '两次密码不一致!', 'code': ''}]}
那么如果我只想把错误信息放在一个列表中,而不要再放在一个字典中。这时候我们可以定义一个方法,把这个数据重新整理一份。实例代码如下:
# form.py
from django.core import validators
class MyForm(forms.Form):
telephone = forms.CharField(
validators=[
validators.RegexValidator(
"1[345678]\d{9}", message="请输入正确格式的手机号码!"
)
]
)
pwd1 = forms.CharField(min_length=6, max_length=100)
pwd2 = forms.CharField(min_length=6, max_length=100)
# get_errors 不会自动调用,因为 Django 已经通过 form.errors 提供了标准接口。
# 如果需要自定义错误格式,手动在视图中调用它即可。
def get_errors(self):
errors = self.errors.get_json_data()
# errors: {'__all__': [{'message': '两次密码不一致!', 'code': ''}]}
print("errors: ", errors)
new_errors = {}
for key, message_dicts in errors.items():
# key: __all__ message_dicts: [{'message': '两次密码不一致!', 'code': ''}]
print("key:", key, "message_dicts:", message_dicts)
messages = []
for message in message_dicts:
messages.append(message["message"])
new_errors[key] = messages
# new_errors: {'__all__': ['两次密码不一致!']} type <class 'dict'>
print("new_errors: ", new_errors, "type", type(new_errors))
return new_errors
# views.py
import json
from .form import MyForm
@require_http_methods(["GET", "POST"])
def myfrom(request):
if request.method == "GET":
return render(request, "myform.html")
else:
form = MyForm(request.POST)
if form.is_valid():
telephone = form.cleaned_data.get("telephone")
return HttpResponse(telephone)
else:
print(json.loads(form.errors.as_json()))
# get_errors 不会自动调用,因为 Django 已经通过 form.errors 提供了标准接口。
# 如果需要自定义错误格式,手动在视图中调用它即可。
return HttpResponse(form.get_errors().get("__all__"))
ModelForm
基本使用
大家在写表单的时候,会发现表单中的 Field
和模型中的 Field
基本上是一模一样的,而且表单中需要验证的数据,也就是我们模型中需要保存的。那么这时候我们就可以将模型中的字段和表单中的字段进行绑定。
比如现在有个 Article 的模型。示例代码如下:
from django.core import validators
class Article(models.Model):
'''
max_length=5: 表单层面和数据库层面都会限制最长为5个字段
validators=[MinLengthValidator(2)]: 添加验证器,要求标题至少 2 个字符(表单或模型保存时验证)
'''
title = models.CharField(
max_length=5,
validators=[validators.MinLengthValidator(limit_value=2)],
help_text="标题长度需在2-5个字符之间", # 增加说明
)
content = models.TextField(
validators=[validators.MinLengthValidator(limit_value=3)],
help_text="正文至少3个字符", # 增加说明
)
# 指定了auto_now_add=True,那么在表单中可以不用传入这个字段
create_time = models.DateTimeField(auto_now_add=True)
# blank=True,只是表单验证时允许为空,不代表数据库可以为空
category = models.CharField(
max_length=5, blank=False, help_text="文章分类不能为空" # 增加说明
)
那么在写表单的时候,就不需要把 Article 模型中所有的字段都一个个重复写一遍了。示例代码如下:
from .models import Article
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = "__all__"
ArticleForm
是继承自 forms.ModelForm
,然后在表单中定义了一个 Meta
类,在 Meta
类中指定了model=Article
,以及 fields="__all__"
,这样就可以将 Article
模型中所有的字段都复制过来,进行验证。如果只想针对其中几个字段进行验证,那么可以给 fields
指定一个列表,将需要的字段写进去。比如只想验证 title
和 content
,那么可以使用以下代码实现:
from .models import Article
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ['title', 'content']
如果要验证的字段比较多,只是除了少数几个字段不需要验证,那么可以使用 exclude
来代替 fields
。比如我不想验证 category
,那么示例代码如下:
from .models import Article
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
exclude = ['category']
其他部分定义如下:
# views.py
@require_http_methods(["GET", "POST"])
def article_view(request):
if request.method == "GET":
return render(request, "article.html")
else:
form = ArticleForm(request.POST)
if form.is_valid():
# 获取title和content以及create_time,然后创建article模型对象,再存储到数据库中
title = form.cleaned_data.get("title")
content = form.cleaned_data.get("content")
return HttpResponse(f"{title}, {content}")
else:
print(form.errors)
return HttpResponse("表单验证失败!")
# article.html
<body>
<form action="" method="POST">
<div>
<input type="text" name="title" placeholder="请输入标题">
</div>
<div>
<textarea name="content" placeholder="请输入内容" id="" cols="30" rows="10"></textarea>
</div>
<div>
<input type="submit" value="提交">
</div>
</form>
</body>
# urls.py
path("article/", views.article_view, name="articleview"),
自定义错误消息
使用 ModelForm
,因为字段都不是在表单中定义的,而是在模型中定义的,因此一些错误消息无法在字段中定义。那么这时候可以在 Meta
类中,定义 error_messages
,然后把相应的错误消息写到里面去。示例代码如下:
from .models import Article
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = "__all__"
# fields = ['title', 'content']
error_messages = {
"category": {"required": "category不能为空!"},
"title": {
"max_length": "最多不能超过10个字符!",
"min_length": "最少不能少于3个字符!",
},
"content": {
"required": "必须输入content!",
},
}
ModelForm
中常见属性
同时影响数据库和表单的字段属性
这些属性在模型(models.py
)中定义后,会通过 ModelForm
自动传递到表单验证和数据库约束中:
(1) max_length
(字符字段长度限制)
数据库层面:
生成VARCHAR(n)
或CHAR(n)
字段,数据库会拒绝超过长度的数据。表单层面:
表单验证时会检查输入长度(如<input maxlength="100">
和服务器验证)。示例:
title = models.CharField(max_length=100) # 数据库和表单均限制 100 字符
(2) null
和 blank
(空值控制)
数据库层面:
null=True
:允许数据库存储NULL
。null=False
(默认):禁止NULL
。
表单层面:
blank=True
:表单验证允许字段为空(对应required=False
)。blank=False
(默认):表单要求必填(对应required=True
)。
示例:
description = models.TextField(null=True, blank=True) # 数据库可NULL,表单可选填
(3) choices
(选项限制)
数据库层面:
仅允许存储定义的选项值(数据库不强制,但 Django 会验证)。表单层面:
渲染为<select>
下拉框,只允许选择预定义的选项。示例:
STATUS_CHOICES = [("draft", "草稿"), ("published", "已发布")] status = models.CharField(max_length=10, choices=STATUS_CHOICES, default="draft")
(4) default
(默认值)
数据库层面:
插入数据时,若未指定值,数据库使用该默认值。表单层面:
ModelForm
会将默认值作为表单字段的initial
值(渲染时预填充)。示例:
is_pinned = models.BooleanField(default=False) # 数据库默认False,表单默认未勾选
(5) unique
(唯一约束)
数据库层面:
创建数据库唯一索引,禁止重复值。表单层面:
表单验证时会检查唯一性(需调用is_valid()
)。示例:
username = models.CharField(max_length=30, unique=True) # 数据库和表单均强制唯一
仅影响数据库或表单的属性
属性 | 作用层面 | 说明 |
---|---|---|
validators |
表单层面 | 仅表单验证时触发,不影响数据库约束 |
auto_now /auto_now_add |
数据库层面 | 自动设置时间,表单通常忽略此类字段 |
verbose_name |
表单层面 | 仅影响表单标签显示,与数据库无关 |
db_index |
数据库层面 | 创建数据库索引,不影响表单行为 |
save方法
ModelForm
还有 save
方法,可以在验证完成后直接调用 save
方法,就可以将这个数据保存到数据库中了。示例代码如下:
@require_http_methods(["GET", "POST"])
def article_view(request):
if request.method == "GET":
return render(request, "article.html")
else:
form = ArticleForm(request.POST)
if form.is_valid():
form.save() ### 这里存入数据库
return HttpResponse("success")
else:
print(form.errors)
return HttpResponse("表单验证失败!")
这个方法必须要在 clean
没有问题后才能使用,如果在 clean
之前使用,会抛出异常。
另外,我们在调用 save
方法的时候,如果传入一个 commit=False
,那么只会生成这个模型的对象,而不会把这个对象真正的插入到数据库中。比如表单上验证的字段没有包含模型中所有的字段,这时候就可以先创建对象,再根据填充其他字段,把所有字段的值都补充完成后,再保存到数据库中。示例代码如下:
@require_http_methods(["GET", "POST"])
def article_view(request):
if request.method == "GET":
return render(request, "article.html")
else:
form = ArticleForm(request.POST)
if form.is_valid():
article = form.save(commit=False) # 这里还没存入数据库,只是生成对象
article.category = 'Python' # 添加字段
article.save() # 存入数据库
return HttpResponse("success")
else:
print(form.errors)
return HttpResponse("表单验证失败!")
文件上传 todo
cookie和session
Cookie介绍
cookie
:在网站中,http请求是无状态的。也就是说即使第一次和服务器连接后并且登录成功后,第二次请求服务器依然不能知道当前请求是哪个用户。 cookie
的出现就是为了解决这个问题,第一次登录后服务器返回一些数据(cookie
)给浏览器,然后浏览器保存在本地,当该用户发送第二次请求的时候,就会自动的把上次请求存储的 cookie
数据自动的携带给服务器,服务器通过浏览器携带的数据就能判断当前用户是哪个了。 cookie
存储的数据量有限,不同的浏览器有不同的存储大小,但一般不超过4KB
。因此使用 cookie
只能存储一些小量的数据。
session
: session
和cookie
的作用有点类似,都是为了存储用户相关的信息。不同的是, cookie
是存储在本地浏览器, session
是一个思路、一个概念、一个服务器存储授权信息的解决方案,不同的服务器,不同的框架,不同的语言有不同的实现。虽然实现不一样,但是他们的目的都是服务器为了方便存储数据的。 session
的出现,是为了解决 cookie
存储数据不安全的问题的。
cookie
和session
使用: web
开发发展至今, cookie
和 session
的使用已经出现了一些非常成熟的方案。在如今的市场或者企业里,一般有两种存储方式:
存储在服务端:通过
cookie
存储一个sessionid
,然后具体的数据则是保存在session
中。如果用户已经登录,则服务器会在cookie
中保存一个sessionid
,下次再次请求的时候,会把该sessionid
携带上来,服务器根据sessionid
在session
库中获取用户的session
数据。就能知道该用户到底是谁,以及之前保存的一些状态信息。这种专业术语叫做server side session
。Django
把session
信息默认存储到数据库中,当然也可以存储到其他地方,比如缓存中,文件系统中等。存储在服务器的数据会更加的安全,不容易被窃取。但存储在服务器也有一定的弊端,就是会占用服务器的资源,但现在服务器已经发展至今,一些session
信息还是绰绰有余的。将
session
数据加密,然后存储在cookie
中。这种专业术语叫做client side session
。flask
框架默认采用的就是这种方式,但是也可以替换成其他形式。
在django中操作cookie
设置cookie:
设置 cookie 是设置值给浏览器的。因此我们需要通过 response 的对象来设置,设置 cookie 可以通过response.set_cookie 来设置,这个方法的相关参数如下:
key
:这个 cookie 的 key 。value
:这个 cookie 的 value 。max_age
:最长的生命周期。单位是秒。expires
:过期时间。跟max_age
是类似的,只不过这个参数需要传递一个具体的日期,比如datetime
或者是符合日期格式的字符串。如果同时设置了expires
和max_age
,那么将会使用expires
的值作为过期时间。path
:对域名下哪个路径有效。默认是对域名下所有路径都有效。domain
:针对哪个域名有效。默认是针对主域名下都有效,如果只要针对某个子域名才有效,那么可以设置这个属性.secure
:是否是安全的,如果设置为 True ,那么只能在 https 协议下才可用。httponly
:默认是 False 。如果为 True ,那么在客户端不能通过 JavaScript 进行操作。
删除cookie
通过 delete_cookie
即可删除 cookie
。实际上删除 cookie
就是将指定的 cookie
的值设置为空的字符串,然后使用将他的过期时间设置为 0
,也就是浏览器关闭后就过期。
获取cookie
获取浏览器发送过来的 cookie
信息。可以通过 request.COOKIES
来获取。这个对象是一个字典类型。比如获取所有的 cookie
,那么示例代码如下:
cookies = request.COOKIES
for cookie_key,cookie_value in cookies.items():
print(cookie_key,cookie_value)
代码演示
我们需要先修改时区设置
# 指定 Django 项目的默认时区(这里是东八区,中国标准时间)。
TIME_ZONE = 'Asia/Shanghai'
# 禁用 Django 的时区感知模式(Time Zone Awareness)。避免因时区转换导致的复杂性问题(适合仅服务单一地区的应用)。
USE_TZ = False
以下是views.py的代码:
from django.shortcuts import HttpResponse
def add_cookie(request):
response = HttpResponse('设置cookie')
max_age = 60*60*24*7
response.set_cookie('username', 'zhiliao', max_age=max_age)
return response
def delete_cookie(request):
response = HttpResponse("删除cookie")
response.delete_cookie('username')
return response
def get_cookie(request):
# username = request.COOKIES.get('username')
# print(username)
for key, value in request.COOKIES.items():
print(key, value)
return HttpResponse("get cookie")
在Django中操作session
常用方法
django
中的 session
默认情况下是存储在服务器的数据库中的,在表中会根据 sessionid
来提取指定的 session
数据,然后再把这个 sessionid
放到 cookie
中发送给浏览器存储,浏览器下次在向服务器发送请求的时候会自动的把所有cookie
信息都发送给服务器,服务器再从 cookie
中获取 sessionid
,然后再从数据库中获取 session
数据。但是我们在操作 session
的时候,这些细节压根就不用管。我们只需要通过 request.session
即可操作。session
常用的方法如下:
get
:用来从session
中获取指定值。pop
:从session
中删除一个值。keys
:从session
中获取所有的键。items
:从session
中获取所有的值。clear
:清除当前这个用户的session
数据。flush
:删除session
并且删除在浏览器中存储的session_id
,一般在注销的时候用得比较多。set_expiry(value)
:设置过期时间。整型
:代表秒数,表示多少秒后过期。0
:代表只要浏览器关闭, session 就会过期。None
:会使用全局的session
配置。在settings.py
中可以设置SESSION_COOKIE_AGE
来配置全局的过期时间。默认是1209600
秒,也就是2周
的时间。
clear_expired
:清除过期的session
。Django
并不会清除过期的session
,需要定期手动的清理,或者是在终端,使用命令行python manage.py clearsessions
来清除过期的session
。
代码示例
生成session表
在我们python项目中有如下默认的APP
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
其中,django.contrib.sessions
就是与session
相关的APP。因此我们需要先运行 python manage.py migrate
命令,生成相关的session表。
在这里我们使用默认的sqlite3
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
简单介绍SQLite
SQLite3 是一个轻量级的、开源的 嵌入式关系型数据库,它以库的形式直接集成在应用程序中,无需独立的服务器进程。以下是SQLite和MySql的区别:
对比项 | SQLite | MySQL | 相同点 |
---|---|---|---|
数据库类型 | 嵌入式数据库(单文件) | 客户端-服务器数据库 | 都是关系型数据库(RDBMS),支持 SQL 语法 |
是否需要独立服务 | 无服务进程,直接读写文件 | 需独立运行 mysqld 服务 |
— |
部署复杂度 | 零配置,开箱即用 | 需安装、配置、管理 | — |
数据存储方式 | 单文件(如 .db 或 .sqlite ) |
多文件存储(表空间、日志等) | — |
并发支持 | 仅支持单线程写入(锁整个数据库文件) | 支持高并发(行级锁、多线程) | — |
网络支持 | 仅本地访问 | 支持远程连接(TCP/IP) | — |
用户权限管理 | 无 | 支持多用户和精细权限控制 | — |
适用场景 | 移动端、嵌入式设备、小型应用、开发测试 | 中大型 Web 应用、企业级系统、高并发生产环境 | 均可用于数据持久化 |
性能 | 读写快(无网络延迟) | 受网络和服务器负载影响 | — |
扩展性 | 功能有限(无存储过程、触发器等高级特性) | 支持存储过程、触发器、视图等 | — |
数据迁移 | 直接复制文件即可 | 需导出 SQL 或使用备份工具 | — |
典型使用者 | Android/iOS、桌面软件、Django 开发调试 | WordPress、电商网站、云服务 | — |
以下是优缺点总结:
对比维度 | SQLite | MySQL |
---|---|---|
部署复杂度 | ✅ 零配置,单文件嵌入,无需安装服务 | ❌ 需独立安装服务端,配置较复杂 |
资源占用 | ✅ 极低(代码库仅几百KB) | ❌ 较高(需独立进程和内存) |
读写速度 | ✅ 快(直接文件操作,无网络延迟) | ⚠️ 受网络和服务器负载影响 |
数据迁移便利性 | ✅ 直接复制文件即可迁移 | ❌ 需导出SQL或专用工具备份 |
并发支持 | ❌ 仅单线程写入(锁全库) | ✅ 支持高并发(行级锁、连接池) |
网络支持 | ❌ 仅本地访问 | ✅ 支持远程TCP/IP连接 |
用户权限管理 | ❌ 无 | ✅ 完善的账户和权限体系 |
高级功能 | ❌ 无存储过程/触发器 | ✅ 支持视图/存储过程/触发器等 |
数据容量 | ❌ 单文件限制(通常不超过128TB) | ✅ 支持分布式和分表分库 |
典型应用场景 | 移动端/嵌入式/开发测试 | 中大型Web应用/企业系统 |
使用Navicat打开SQLite数据库:
session表
session表只有3个字段
代码
from django.shortcuts import HttpResponse
def add_session(request):
# 如果没有设置session过期时间,默认是2周后过期
request.session['user_id'] = 'zhiliao'
# 如果设置成0,那么浏览器关闭后,session就会过期
request.session.set_expiry(0)
return HttpResponse("session add")
def get_session(request):
username = request.session.get('user_id')
print(username)
return HttpResponse('get session')
访问add_session
之后,会在django_session
表中生成提条数据,session_key
和session_data
都进行了加密。
然后把session_key
的值赋值给sessionId
存在浏览器的cookie
中:
访问get_session
之后就可以获取user_id
的值
修改session的存储机制
默认情况下,session
数据是存储到数据库中的。当然也可以将 session
数据存储到其他地方。可以通过设置 SESSION_ENGINE
来更改 session
的存储位置,这个可以配置为以下几种方案:
django.contrib.sessions.backends.db
:使用数据库。默认就是这种方案。django.contrib.sessions.backends.file
:使用文件来存储session。django.contrib.sessions.backends.cache
:使用缓存来存储session
。想要将数据存储到缓存中,前提是你必须要在settings.py
中配置好CACHES
,并且是需要使用Memcached
,而不能使用纯内存作为缓存。django.contrib.sessions.backends.cached_db
:在存储数据的时候,会将数据先存到缓存中,再存到数据库中。这样就可以保证万一缓存系统出现问题,session
数据也不会丢失。在获取数据的时候,会先从缓存中获取,如果缓存中没有,那么就会从数据库中获取。django.contrib.sessions.backends.signed_cookies
:将session
信息加密后存储到浏览器的cookie
中。这种方式要注意安全,建议设置SESSION_COOKIE_HTTPONLY=True
,那么在浏览器中不能通过js
来操作session
数据,并且还需要对settings.py
中的SECRET_KEY
进行保密,因为一旦别人知道这个SECRET_KEY
,那么就可以进行解密。另外还有就是在cookie
中,存储的数据不能超过4k
。
CSRF攻击 todo
CSRF攻击概述:
CSRF
(Cross Site Request Forgery
,跨域请求伪造)是一种网络的攻击方式,它在 2007 年曾被列为互联网 20 大安全隐患之一。
其他安全隐患,比如 SQL脚本注入,跨域脚本攻击等在近年来已经逐渐为众人熟知,很多网站也都针对他们进行了防御。然而,对于大多数人来说,CSRF 却依然是一个陌生的概念。
即便是大名鼎鼎的 Gmail,在 2007 年底也存在着 CSRF 漏洞,从而被黑客攻击而使 Gmail 的用户造成巨大的损失
CSRF攻击原理
网站是通过 cookie来实现登录功能的。
而 cookie只要存在浏览器中,那么浏览器在访问这个cookie的服务器的时候,就会自动的携带 cookie信息到服务器上去。
那么这时候就存在一个漏洞了,如果你访问了一个别有用心的病毒网站,这个网站可以在网页源代码中插入js
代码,使用js
代码给其他服务器发送请求(比如ICBC的转账请求)。那么因为在发送请求的时候,浏览器会自动的把,cookie
发送给对应的服务器,这时候相应的服务器(比如ICBC网站),就不知道这个请求是伪造的,就被欺骗过去了。从而达到在用户不知情的情况下,给某个服务器发送了一个请求(比如转账)。
防御CSRF攻击
CSRF
攻击的要点就是在向服务器发送请求的时候,相应的 cookie
会自动的发送给对应的服务器。造成服务器不知道这个请求是用户发起的还是伪造的。
这时候,我们可以在用户每次访问有表单的页面的时候,在网页源代码中加一个随机的字符串叫做csrf_token
,在 cookie
中也加入 csrf_token
字符串。
**以后给服务器发送请求的时候,必须在 form
中以及 cookie
中都携带 csrf_token
,服务器只有检测到 cookie
中的csrf_token
和 form
中的csrf_token
都匹配(不是相等),才认为这个请求是正常的,否则就是伪造的。**那么黑客就没办法伪造请求了。
在 Django 中,如果想要防御 CSRF攻击,应该做两步工作。
第一个是在
settings.MIDDLEWARE
中添加csrfiddleware
中间件。MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', # `csrfiddleware`中间件 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ]
第二个是在模版代码中添加一个
input
标签,加载csrf_token
。<input type="hidden"name="csrfmiddlewaretoken" value="{{ csrf_token }}"/>
或者是直接在模版代码中使用
csrf_token
标签,来自动生成一个带有csrf token
的input
标签:{% csrf_token %}
代码演示
以下是views.py的代码,简单实现登录效果
from django.shortcuts import render, HttpResponse
from django.views.decorators.http import require_http_methods
@require_http_methods(['GET', 'POST'])
def login(request):
if request.method == 'GET':
return render(request, 'login.html')
else:
print(request.POST)
print(request.COOKIES)
return HttpResponse("登录")
以下是login.html的代码,目前没有加csrf_token
,看看会发生什么
<form action="" method="POST">
<table>
<tbody>
<tr>
<td>邮箱:</td>
<td><input type="email" name="email"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="登录"></td>
</tr>
</tbody>
</table>
</form>
点击登录,会发生如下报错:
Forbidden (CSRF cookie not set.): /login
[14/May/2025 04:54:59] "POST /login HTTP/1.1" 403 2855
这是因为,我们在settings.py
里面开启了django.middleware.csrf.CsrfViewMiddleware
的中间件,那么服务器回去检查csrf_token
。
因此我们需要修改模版代码:
<form action="" method="POST">
{% comment %} <input type="hidden"name="csrfmiddlewaretoken" value="{{ csrf_token }}"/> {% endcomment %}
{% csrf_token %}
<table>
<tbody>
<tr>
<td>邮箱:</td>
<td><input type="email" name="email"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="登录"></td>
</tr>
</tbody>
</table>
</form>
这个时候,我么访问表单的时候就会自动生成csrf_token
。再次点击登陆,就会登陆成功。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1909773034@qq.com