八、Django的组件
8.1、中间件
中间件顾名思义,是介于request与response处理之间的一道处理过程,相对比较轻量级,并且在全局上改变django的输入与输出。因为改变的是全局,所以需要谨慎实用,用不好会影响到性能。
Django的中间件的定义:
Middleware is a framework of hooks into Django’s request/response processing.
It’s a light, low-level “plugin” system for globally altering Django’s input or output.
MiddleWare,是 Django 请求/响应处理的钩子框架。
它是一个轻量级的、低级的“插件”系统,用于全局改变 Django 的输入或输出。【输入指代的就是客户端像服务端django发送数据,输出指代django根据客户端要求处理数据的结果返回给客户端】
如果你想修改请求,例如被传送到view中的HttpRequest对象。 或者你想修改view返回的HttpResponse对象,这些都可以通过中间件来实现。
django框架内部声明了很多的中间件,这些中间件有着各种各种的用途,有些没有被使用,有些被默认开启使用了。而被开启使用的中间件,都是在settngs.py的MIDDLEWARE中注册使用的。
Django默认的Middleware
:
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',
]
8.1.1.自定义中间件
(1)定义中间件
创建存放自定义中间件的文件这里选择在app01里创建mdws.py文件:
from django.utils.deprecation import MiddlewareMixin
class Md1(MiddlewareMixin):
def process_request(self, request):
print("Md1请求")
# return HttpResponse("Md1中断") # 拦截
def process_response(self, request, response):
print("Md1返回")
return response
class Md2(MiddlewareMixin):
def process_request(self, request):
print("Md2请求")
# return HttpResponse("Md2中断")
def process_response(self, request, response):
print("Md2返回")
return response
- process_request默认返回None,返回None,则继续执行下一个中间件的process_request;一旦返回响应体对象,则会拦截返回。
- process_response必须有一个形参response,并return response;这是view函数返回的响应体,像接力棒一样传承给最后的客户端。
(2) 注册中间件
MIDDLEWARE = [
...
'app01.mdws.Md1',
'app01.mdws.Md2'
]
(3)构建index路由
# path('index/', views.index),
def index(request):
print("index 视图函数执行...")
return HttpResponse("hello yuan")
后台打印结果:
Md1请求
Md2请求
index 视图函数执行...
Md2返回
Md1返回
所以,通过结果我们看出中间件的执行顺序:
8.1.3.中间件应用
1. 做IP访问频率限制
某些IP访问服务器的频率过高,进行拦截,比如限制每分钟不能超过20次。
8.2、Cookie与Session
我们知道HTTP协议是无状态协议,也就是说每个请求都是独立的!无法记录前一次请求的状态。但HTTP协议中可以使用Cookie来完成会话跟踪!在Web开发中,使用session来完成会话跟踪,session底层依赖Cookie技术。
8.2.1、cookie
Cookie翻译成中文是小甜点,小饼干的意思。在HTTP中它表示服务器送给客户端浏览器的小甜点。其实Cookie是key-value结构,类似于一个python中的字典。随着服务器端的响应发送给客户端浏览器。然后客户端浏览器会把Cookie保存起来,当下一次再访问服务器时把Cookie再发送给服务器。 Cookie是由服务器创建,然后通过响应发送给客户端的一个键值对。客户端会保存Cookie,并会标注出Cookie的来源(哪个服务器的Cookie)。当客户端向服务器发出请求时会把所有这个服务器Cookie包含在请求中发送给服务器,这样服务器就可以识别客户端了!
cookie可以理解为每一个浏览器针对每一个服务器创建的key-value结构的本地存储文件
(1)cookie流程图
(2)cookie语法
# (1) 设置cookie:
res = HttpResponse(...) 或 rep = render(request, ...) 或 rep = redirect()
res.set_cookie(key,value,max_age...)
res.set_signed_cookie(key,value,salt='加密盐',...)
# (2) 获取cookie:
request.COOKIES
# (3) 删除cookie
response.delete_cookie("cookie_key",path="/",domain=name)
8.2.2、session
Django 提供对匿名会话(session)的完全支持。这个会话框架让你可以存储和取回每个站点访客任意数据。它在服务器端存储数据, 并以cookies的形式进行发送和接受数据。
(1)session流程图
(2)session语法与案例
# 1、设置Sessions值
request.session['session_name'] ="admin"
# 2、获取Sessions值
session_name = request.session["session_name"]
# 3、删除Sessions值
del request.session["session_name"]
# 4、flush()
# 删除当前的会话数据并删除会话的Cookie。这用于确保前面的会话数据不可以再次被用户的浏览器访问
def s_login(request):
if request.method == "GET":
return render(request, "login.html")
else:
user = request.POST.get("user")
pwd = request.POST.get("pwd")
try:
# user_obj = User.objects.get(user=user,pwd=pwd)
# 写session
# request.session["is_login"] = True
# request.session["username"] = user_obj.user
return redirect("/s_index/")
except:
return redirect("/s_login/")
def s_index(request):
# 读session
is_login = request.session.get("is_login")
if is_login:
username = request.session.get("username")
return render(request, "index.html", {"user": username})
else:
return redirect("/s_login/")
'''
shop.html:
<p>
客户端最后一次访问时间:{{ last_time|default:"第一次访问" }}
</p>
<h3>商品页面</h3>
'''
def shop(request):
last_time = request.session.get("last_time")
now = datetime.datetime.now().strftime("%Y-%m-%d %X")
request.session["last_time"] = now
return render(request, "shop.html", {"last_time": last_time})
def s_logout(request):
# request.session.flush()
del request.session["username"]
del request.session["is_login"]
return redirect("/s_login/")
- session 在服务器端,cookie 在客户端(浏览器)
- session 默认被存在在服务器的一个文件里(不是内存)
- session 的运行依赖 session id,而 session id 是存在 cookie 中的.
- session 可以放在 文件、数据库、或内存中都可以。
- 用户验证这种场合一般会用 session
(3)session配置
# Django默认支持Session,并且默认是将Session数据存储在数据库中,即:django_session 表中。
# 配置 settings.py
SESSION_ENGINE = 'django.contrib.sessions.backends.db' # 引擎(默认)
SESSION_COOKIE_NAME = "sessionid" # Session的cookie保存在浏览器上时的key,即:sessionid=随机字符串(默认)
SESSION_COOKIE_PATH = "/" # Session的cookie保存的路径(默认)
SESSION_COOKIE_DOMAIN = None # Session的cookie保存的域名(默认)
SESSION_COOKIE_SECURE = False # 是否Https传输cookie(默认)
SESSION_COOKIE_HTTPONLY = True # 是否Session的cookie只支持http传输(默认)
SESSION_COOKIE_AGE = 1209600 # Session的cookie失效日期(2周)(默认)
SESSION_EXPIRE_AT_BROWSER_CLOSE = False # 是否关闭浏览器使得Session过期(默认)
SESSION_SAVE_EVERY_REQUEST = False # 是否每次请求都保存Session,默认修改之后才保存(默认)
8.2.3、用户认证组件
Django默认已经提供了认证系统Auth模块,我们认证的时候,会使用auth模块里面给我们提供的表。认证系统包含:
- 用户管理
- 权限
- 用户组
- 密码哈希系统
- 用户登录或内容显示的表单和视图
- 一个可插拔的后台系统 admin
(1)Django用户模型类
Django认证系统中提供了用户模型类User保存用户的数据,默认的User包含以下常见的基本字段:
字段名 | 字段描述 |
---|---|
username |
必选。150个字符以内。 用户名可能包含字母数字,_ ,@ ,+ . 和- 个字符。 |
first_name |
可选(blank=True )。 少于等于30个字符。 |
last_name |
可选(blank=True )。 少于等于30个字符。 |
email |
可选(blank=True )。 邮箱地址。 |
password |
必选。 密码的哈希加密串。 (Django 不保存原始密码)。 原始密码可以无限长而且可以包含任意字符。 |
groups |
与Group 之间的多对多关系。 |
user_permissions |
与Permission 之间的多对多关系。 |
is_staff |
布尔值。 设置用户是否可以访问Admin 站点。 |
is_active |
布尔值。 指示用户的账号是否激活。 它不是用来控制用户是否能够登录,而是描述一种帐号的使用状态。 |
is_superuser |
是否是超级用户。超级用户具有所有权限。 |
last_login |
用户最后一次登录的时间。 |
date_joined |
账户创建的时间。 当账号创建时,默认设置为当前的date/time。 |
上面缺少一些字段,所以后面我们会对当前内置的用户模型进行改造,比如说它里面没有手机号字段,后面我们需要加上。
(2)重要方法
Django 用户认证(Auth)组件需要导入 auth 模块
# 认证模块
from django.contrib import auth
# 对应数据库用户表,可以继承扩展
from django.contrib.auth.models import User
(1)用户对象
create() # 创建一个普通用户,密码是明文的。
create_user() # 创建一个普通用户,密码是密文的。
create_superuser() # 与create_user() 相同,但是设置is_staff 和is_superuser 为True。
set_password(*raw_password*)
# 设置用户的密码为给定的原始字符串,并负责密码的。 不会保存User对象。当None为raw_password时,密码将设置为一个不可用的密码。
check_password(*raw_password*)
# 如果给定的raw_password是用户的真实密码,则返回True,可以在校验用户密码时使用。
(2)认证方法
auth.authenticate(username,password)
# 将输入的密码转为密文去认证,认证成功返回用户对象,失败则返回None
(3)登录和注销方法
from django.contrib import auth
# 该函数接受一个HttpRequest对象,以及一个认证了的User对象。此函数使用django的session框架给某个已认证的用户附加上session id等信息。
auth.login()
# 该函数接受一个HttpRequest对象,无返回值。当调用该函数时,当前请求的session信息会全部清除。该用户即使没有登录,使用该函数也不会报错。
auth.logout()
(4)request.user
|
(5)自定义用户表
from django.contrib.auth.models import AbstractUser
设置Auth认证模块使用的用户模型为我们自己定义的用户模型
格式:“子应用目录名.模型类名”
AUTH_USER_MODEL = ‘users.User’
8.3、Django的分页器
(1) index视图
def index(request):
'''
批量导入数据:
Booklist=[]
for i in range(100):
Booklist.append(Book(title="book"+str(i),price=30+i*i))
Book.objects.bulk_create(Booklist)
分页器的使用:
book_list=Book.objects.all()
paginator = Paginator(book_list, 10)
print("count:",paginator.count) #数据总数
print("num_pages",paginator.num_pages) #总页数
print("page_range",paginator.page_range) #页码的列表
page1=paginator.page(1) # 第1页的page对象
for i in page1: # 遍历第1页的所有数据对象
print(i)
print(page1.object_list) #第1页的所有数据
page2=paginator.page(2)
print(page2.has_next()) #是否有下一页
print(page2.next_page_number()) #下一页的页码
print(page2.has_previous()) #是否有上一页
print(page2.previous_page_number()) #上一页的页码
# 抛错
#page=paginator.page(12) # error:EmptyPage
#page=paginator.page("z") # error:PageNotAnInteger
'''
book_list = Book.objects.all()
paginator = Paginator(book_list, 10)
page = request.GET.get('page', 1)
current_page = int(page)
try:
print(page)
book_list = paginator.page(page)
except PageNotAnInteger:
book_list = paginator.page(1)
except EmptyPage:
book_list = paginator.page(paginator.num_pages)
return render(request, "index.html", {"book_list": book_list, "paginator": paginator, "currentPage": current_page})
(2) index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>
<body>
<div class="container">
<h4>分页器</h4>
<ul>
{% for book in book_list %}
<li>{{ book.title }} -----{{ book.price }}</li>
{% endfor %}
</ul>
<ul class="pagination" id="pager">
{% if book_list.has_previous %}
<li class="previous"><a href="/index/?page={{ book_list.previous_page_number }}">上一页</a></li>
{% else %}
<li class="previous disabled"><a href="#">上一页</a></li>
{% endif %}
{% for num in paginator.page_range %}
{% if num == currentPage %}
<li class="item active"><a href="/index/?page={{ num }}">{{ num }}</a></li>
{% else %}
<li class="item"><a href="/index/?page={{ num }}">{{ num }}</a></li>
{% endif %}
{% endfor %}
{% if book_list.has_next %}
<li class="next"><a href="/index/?page={{ book_list.next_page_number }}">下一页</a></li>
{% else %}
<li class="next disabled"><a href="#">下一页</a></li>
{% endif %}
</ul>
</div>
</body>
</html>
8.4、FBV与CBV
1 FBV :function based view
2 BCV:class based view
8.4.1、前后端分离模式
在开发Web应用中,有两种应用模式:
- 前后端不分离[客户端看到的内容和所有界面效果都是由服务端提供出来的。]
- 前后端分离【把前端的界面效果(html,css,js分离到另一个服务端,python服务端只需要返回数据即可)】
前端形成一个独立的网站,服务端构成一个独立的网站
8.4.2、api接口
应用程序编程接口(Application Programming Interface,API接口),就是应用程序对外提供了一个操作数据的入口,这个入口可以是一个函数或类方法,也可以是一个url地址或者一个网络地址。当客户端调用这个入口,应用程序则会执行对应代码操作,给客户端完成相对应的功能。
当然,api接口在工作中是比较常见的开发内容,有时候,我们会调用其他人编写的api接口,有时候,我们也需要提供api接口给其他人操作。由此就会带来一个问题,api接口往往都是一个函数、类方法、或者url或其他网络地址,不断是哪一种,当api接口编写过程中,我们都要考虑一个问题就是这个接口应该怎么编写?接口怎么写的更加容易维护和清晰,这就需要大家在调用或者编写api接口的时候要有一个明确的编写规范!!!
为了在团队内部形成共识、防止个人习惯差异引起的混乱,我们都需要找到一种大家都觉得很好的接口实现规范,而且这种规范能够让后端写的接口,用途一目了然,减少客户端和服务端双方之间的合作成本。
目前市面上大部分公司开发人员使用的接口实现规范主要有:restful、RPC。
RPC( Remote Procedure Call ): 翻译成中文:远程过程调用[远程服务调用]. 从字面上理解就是访问/调用远程服务端提供的api接口。这种接口一般以服务或者过程式代码提供。
-
服务端提供一个唯一的访问入口地址:http://api.xxx.com/ 或 http://www.xx.com/api 或者基于其他协议的地址
-
客户端请求服务端的时候,所有的操作都理解为动作(action),一般web开发时,对应的就是HTTP请求的post请求
-
通过请求体参数,指定要调用的接口名称和接口所需的参数
action=get_all_student&class=301&sex=1
m=get_all_student&sex=1&age=22
command=100&sex=1&age=22
rpc接口多了,对应函数名和参数就多了,前端在请求api接口时难找.对于年代久远的rpc服务端的代码也容易出现重复的接口
restful: 翻译成中文: 资源状态转换.(表征性状态转移)
-
把服务端提供的所有的数据/文件都看成资源, 那么通过api接口请求数据的操作,本质上来说就是对资源的操作了.
因此,restful中要求,我们把当前接口对外提供哪种资源进行操作,就把资源的名称写在url地址。
-
web开发中操作资源,最常见的最通用的无非就是增删查改,所以restful要求在地址栏中声明要操作的资源是什么。然后通过http请求动词来说明对该资源进行哪一种操作.
POST http://www.xxx.com/api/students/ 添加学生数据
GET http://www.xxx.com/api/students/ 获取所有学生
GET http://www.xxx.com/api/students/1/ 获取id=pk的学生
DELETE http://www.xxx.com/api/students/1/ 删除id=pk的一个学生
PUT http://www.xxx.com/api/students/1/ 修改一个学生的全部信息 [id,name,sex,age,]
PATCH http://www.xxx.com/api/students/1/ 修改一个学生的部分信息[age]
也就是说,我们仅需要通过url地址上的资源名称结合HTTP请求动作,就可以说明当前api接口的功能是什么了。restful是以资源为主的api接口规范,体现在地址上就是资源就是以名词表达。rpc则以动作为主的api接口规范,体现在接口名称上往往附带操作数据的动作。
8.4.3、RESTful API规范
REST全称是Representational State Transfer,中文意思是表述(编者注:通常译为表征)性状态转移。 它首次出现在2000年Roy Fielding的博士论文中。
RESTful是一种专门为Web 开发而定义API接口的设计风格,尤其适用于前后端分离的应用模式中。
这种风格的理念认为后端开发任务就是提供数据的,对外提供的是数据资源的访问接口,所以在定义接口时,客户端访问的URL路径就表示这种要操作的数据资源。
而对于数据资源分别使用POST、DELETE、GET、UPDATE等请求动作来表达对数据的增删查改。
GET | /students | 获取所有学生 |
---|---|---|
请求方法 | 请求地址 | 后端操作 |
POST | /students | 增加学生 |
GET | /students/ | 获取编号为pk的学生 |
PUT | /students/ | 修改编号为pk的学生 |
DELETE | /students/ | 删除编号为pk的学生 |
restful规范是一种通用的规范,不限制语言和开发框架的使用。事实上,我们可以使用任何一门语言,任何一个框架都可以实现符合restful规范的API接口。
参考文档:http://www.runoob.com/w3cnote/restful-architecture.html
接口实现过程中,会存在幂等性。所谓幂等性是指代客户端发起多次同样请求时,是否对于服务端里面的资源产生不同结果。如果多次请求,服务端结果还是一样,则属于幂等接口,如果多次请求,服务端产生结果是不一样的,则属于非幂等接口。
请求方式 | 是否幂等 | 是否安全 |
---|---|---|
GET | 幂等 | 安全 |
POST | 不幂等 | 不安全 |
PUT/PATCH | 幂等 | 不安全 |
DELETE | 幂等 | 不安全 |
8.4.4、CBV使用
之前我们用的视图函数叫FBV(也就是函数型视图函数),这里我们来试试CBV(类视图函数)的写法。类视图函数可以让代码看起来更简洁,用起来更方便。
# FBV
# def index(request):
# if request.method == "GET":
#
# return HttpResponse("GET")
# elif request.method == "POST":
#
# return HttpResponse("POST")
#
# elif request.method == "DELETE":
# return HttpResponse("DELETE")
# CBV模式: 基于restful开发
class IndexView(View):
def get(self, request):
return HttpResponse("CBV GET")
def post(self, request):
return HttpResponse("CBV POST")
class BookView(View):
def get(self, request):
# 获取数据
book_list = Book.objects.all()
# 序列化:json
data_json = serializers.serialize("json", book_list)
return HttpResponse(data_json, content_type="json")
# FBV模式
# path('index/', views.index),
# CBV模式
path("index/",views.IndexView.as_view()),
path("books/",views.BookView.as_view())
8.5、csrftoken(跨站请求伪造)
CSRF(Cross-Site Request Forgery,跨站点伪造请求)是一种网络攻击方式,该攻击可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击站点,从而在未授权的情况下执行在权限保护之下的操作,具有很大的危害性。具体来讲,可以这样理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。
-- 企业邮箱
-- pornhub.com
token其实就是一个令牌,用于用户验证的,token的诞生离不开CSRF。正是由于上面的Cookie/Session的状态保持方式会出现CSRF,所以才有了token。
- 解除中间件注释
- 无csrf_token数据的post请求
8.5.1、基本使用
一、form表单提交
在html页面form表单中直接添加{% csrf_token%}
![屏幕快照 2021-10-31 下午10.30.29](assets/屏幕快照 2021-10-31 下午10.30.29.png)
二、ajax提交
方式1:放在请求数据中。
$.ajax({
url: '/csrf_test/',
method: 'post',
data: {'name': $('[name="name"]').val(),
'password': $('[name="password"]').val(),
'csrfmiddlewaretoken':$('[name="csrfmiddlewaretoken"]').val()
},
success: function (data) {
console.log('成功了')
console.log(data) },
})
方式2:放在请求头
$.ajax({
url: '/csrf_test/',
method: 'post',
headers:{'X-CSRFToken':'token值'}, // 注意放到引号里面
data:{}
}
8.5.2、全局使用,局部禁csrf
(1) 在视图函数上加装饰器
from django.views.decorators.csrf import csrf_exempt,csrf_protect
@csrf_exempt
def 函数名(request): # 加上装饰器后,这个视图函数,就没有csrf校验了
(2) 视图类
from django.views.decorators.csrf import csrf_exempt,csrf_protect
from django.utils.decorators import method_decorator
@method_decorator(csrf_exempt,name='dispatch')
class index(View):
def get(self,request):
return HttpResponse("GET")
def post(self,request):
return HttpResponse("POST")
8.6、ORM进阶
8.6.1、queryset特性
(1)可切片
使用Python 的切片语法来限制查询集
记录的数目 。它等同于SQL 的LIMIT
和OFFSET
子句。
>>> Article.objects.all()[:5] # (LIMIT 5)
>>> Article.objects.all()[5:10] # (OFFSET 5 LIMIT 5)
不支持负的索引(例如Article.objects.all()[-1]
)。通常,查询集
的切片返回一个新的查询集
—— 它不会执行查询。
(2)可迭代
articleList=models.Article.objects.all()
for article in articleList:
print(article.title)
3)惰性查询
查询集
是惰性执行的 —— 创建查询集
不会带来任何数据库的访问。你可以将过滤器保持一整天,直到查询集
需要求值时,Django 才会真正运行这个查询。
queryResult=models.Article.objects.all() # not hits database
print(queryResult) # hits database
for article in queryResult:
print(article.title) # hits database
一般来说,只有在“请求”查询集
的结果时才会到数据库中去获取它们。当你确实需要结果时,查询集
通过访问数据库来求值。 关于求值发生的准确时间。
(4)缓存机制
每个查询集
都包含一个缓存来最小化对数据库的访问。理解它是如何工作的将让你编写最高效的代码。
在一个新创建的查询集
中,缓存为空。首次对查询集
进行求值 —— 同时发生数据库查询 ——Django 将保存查询的结果到查询集
的缓存中并返回明确请求的结果(例如,如果正在迭代查询集
,则返回下一个结果)。接下来对该查询集
的求值将重用缓存的结果。
请牢记这个缓存行为,因为对查询集
使用不当的话,它会坑你的。例如,下面的语句创建两个查询集
,对它们求值,然后扔掉它们:
queryset = Book.objects.all()
print(queryset) # hit database
print(queryset) # hit database
注:简单地打印查询集不会填充缓存。
这意味着相同的数据库查询将执行两次,显然倍增了你的数据库负载。同时,还有可能两个结果列表并不包含相同的数据库记录,因为在两次请求期间有可能有Article被添加进来或删除掉。为了避免这个问题,只需保存查询集
并重新使用它:
queryset = Book.objects.all()
ret = [i for i in queryset] # hit database
print(queryset) # 使用缓存
print(queryset) # 使用缓存
何时查询集会被缓存?
- 遍历queryset时
- if语句(为了避免这个,可以用exists()方法来检查是否有数据)
所以单独queryset的索引或者切片都不会缓存。
queryset = Book.objects.all()
one = queryset[0] # hit database
two = queryset[1] # hit database
print(one)
print(two)
(5)exists()与iterator()方法
exists
简单的使用if语句进行判断也会完全执行整个queryset并且把数据放入cache,虽然你并不需要这些 数据!为了避免这个,可以用exists()方法来检查是否有数据:
if queryResult.exists():
#SELECT (1) AS "a" FROM "blog_article" LIMIT 1; args=()
print("exists...")
iterator
当queryset非常巨大时,cache会成为问题。
处理成千上万的记录时,将它们一次装入内存是很浪费的。更糟糕的是,巨大的queryset可能会锁住系统 进程,让你的程序濒临崩溃。要避免在遍历数据的同时产生queryset cache,可以使用iterator()方法 来获取数据,处理完数据就将其丢弃。
objs = Book.objects.all().iterator()
# iterator()可以一次只从数据库获取少量数据,这样可以节省内存
for obj in objs:
print(obj.title)
#BUT,再次遍历没有打印,因为迭代器已经在上一次遍历(next)到最后一次了,没得遍历了
for obj in objs:
print(obj.title)
当然,使用iterator()方法来防止生成cache,意味着遍历同一个queryset时会重复执行查询。所以使 #用iterator()的时候要当心,确保你的代码在操作一个大的queryset时没有重复执行查询。
queryset的cache是用于减少程序对数据库的查询,在通常的使用下会保证只有在需要的时候才会查询数据库。 使用exists()和iterator()方法可以优化程序对内存的使用。不过,由于它们并不会生成queryset cache,可能 会造成额外的数据库查询。
总结:在使用缓存机制还是生成器机制的选择上如果是,数据量大情况主要使用生成器;数据少使用次数多的情况使用缓存机制。
8.6.2、中介模型
处理类似搭配 pizza 和 topping 这样简单的多对多关系时,使用标准的ManyToManyField
就可以了。但是,有时你可能需要关联数据到两个模型之间的关系上。
例如,有这样一个应用,它记录音乐家所属的音乐小组。我们可以用一个ManyToManyField
表示小组和成员之间的多对多关系。但是,有时你可能想知道更多成员关系的细节,比如成员是何时加入小组的。
对于这些情况,Django 允许你指定一个中介模型来定义多对多关系。 你可以将其他字段放在中介模型里面。源模型的ManyToManyField
字段将使用through
参数指向中介模型。对于上面的音乐小组的例子,代码如下:
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=128)
def __str__(self): # __unicode__ on Python 2
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
def __str__(self): # __unicode__ on Python 2
return self.name
class Membership(models.Model):
person = models.ForeignKey(Person)
group = models.ForeignKey(Group)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
既然你已经设置好ManyToManyField
来使用中介模型(在这个例子中就是Membership
),接下来你要开始创建多对多关系。你要做的就是创建中介模型的实例:
>>> ringo = Person.objects.create(name="Ringo Starr")
>>> paul = Person.objects.create(name="Paul McCartney")
>>> beatles = Group.objects.create(name="The Beatles")
>>> m1 = Membership(person=ringo, group=beatles,
... date_joined=date(1962, 8, 16),
... invite_reason="Needed a new drummer.")
>>> m1.save()
>>> beatles.members.all()
[<Person: Ringo Starr>]
>>> ringo.group_set.all()
[<Group: The Beatles>]
>>> m2 = Membership.objects.create(person=paul, group=beatles,
... date_joined=date(1960, 8, 1),
... invite_reason="Wanted to form a band.")
>>> beatles.members.all()
[<Person: Ringo Starr>, <Person: Paul McCartney>]
与普通的多对多字段不同,你不能使用add
、 create
和赋值语句(比如,beatles.members = [...]
)来创建关系:
# THIS WILL NOT WORK
>>> beatles.members.add(john)
# NEITHER WILL THIS
>>> beatles.members.create(name="George Harrison")
# AND NEITHER WILL THIS
>>> beatles.members = [john, paul, ringo, george]
为什么不能这样做? 这是因为你不能只创建 Person
和 Group
之间的关联关系,你还要指定 Membership
模型中所需要的所有信息;而简单的add
、create
和赋值语句是做不到这一点的。所以它们不能在使用中介模型的多对多关系中使用。此时,唯一的办法就是创建中介模型的实例。
remove()
方法被禁用也是出于同样的原因。但是clear()
方法却是可用的。它可以清空某个实例所有的多对多关系:
>>> # Beatles have broken up
>>> beatles.members.clear()
>>> # Note that this deletes the intermediate model instances
>>> Membership.objects.all()
[]
8.6.3、数据库表反向生成模型类
众所周知,Django较为适合原生开发,即通过该框架搭建一个全新的项目,通过在修改models.py来创建新的数据库表。但是往往有时候,我们需要利用到之前的已经设计好的数据库,数据库中提供了设计好的多种表单。那么这时如果我们再通过models.py再来设计就会浪费很多的时间。所幸Django为我们提供了inspecdb的方法。他的作用即使根据已经存在对的mysql数据库表来反向映射结构到models.py中.
我们在展示django ORM反向生成之前,我们先说一下怎么样正向生成代码。
正向生成,指的是先创建model.py文件,然后通过django内置的编译器,在数据库如mysql中创建出符合model.py的表。
反向生成,指的是先在数据库中create table,然后通过django内置的编译器,生成model代码。
python manage.py inspectdb > models文件名
8.6.4、查询优化
(1)exists()
判断查询集中是否有数据,如果有则返回True,没有则返回False
(2)select_related()
对于一对一字段(OneToOneField)和外键字段(ForeignKey),可以使用select_related 来对QuerySet进行优化。
select_related 返回一个QuerySet
,当执行它的查询时它沿着外键关系查询关联的对象的数据。它会生成一个复杂的查询并引起性能的损耗,但是在以后使用外键关系时将不需要数据库查询。
简单说,在对QuerySet使用select_related()函数后,Django会获取相应外键对应的对象,从而在之后需要的时候不必再查询数据库了。
下面的例子解释了普通查询和select_related()
查询的区别。
查询id=2的的书籍的出版社名称,下面是一个标准的查询:
# Hits the database.
book= models.Book.objects.get(nid=2)
# Hits the database again to get the related Blog object.
print(book.publish.name)
如果我们使用select_related()函数:
books=models.Book.objects.select_related("publish").all()
for book in books:
# Doesn't hit the database, because book.publish
# has been prepopulated in the previous query.
print(book.publish.name)
多外键查询
这是针对publish的外键查询,如果是另外一个外键呢?让我们一起看下:
book=models.Book.objects.select_related("publish").get(nid=1)
print(book.authors.all())
观察logging结果,发现依然需要查询两次,所以需要改为:
book=models.Book.objects.select_related("publish","").get(nid=1)
print(book.publish)
或者:
book=models.Article.objects
.select_related("publish")
.select_related("")
.get(nid=1) # django 1.7 支持链式操作
print(book.publish)
(3)prefetch_related()
对于多对多字段(ManyToManyField)和一对多字段,可以使用prefetch_related()来进行优化。
prefetch_related()和select_related()的设计目的很相似,都是为了减少SQL查询的数量,但是实现的方式不一样。后者是通过JOIN语句,在SQL查询内解决问题。但是对于多对多关系,使用SQL语句解决就显得有些不太明智,因为JOIN得到的表将会很长,会导致SQL语句运行时间的增加和内存占用的增加。若有n个对象,每个对象的多对多字段对应Mi条,就会生成Σ(n)Mi 行的结果表。
prefetch_related()的解决方法是,分别查询每个表,然后用Python处理他们之间的关系。
# 查询所有文章关联的所有标签
books=models.Book.objects.all()
for book in books:
print(book.authors.all()) #4篇文章: hits database 5
改为prefetch_related:
# 查询所有文章关联的所有标签
books=models.Book.objects.prefetch_related("authors").all()
for book in books:
print(book.authors.all()) #4篇文章: hits database 2
(4)extra
extra(select=None, where=None, params=None,
tables=None, order_by=None, select_params=None)
有些情况下,Django的查询语法难以简单的表达复杂的 WHERE
子句,对于这种情况, Django 提供了 extra()
QuerySet
修改机制 — 它能在 QuerySet
生成的SQL从句中注入新子句
extra可以指定一个或多个 参数
,例如 select
, where
or tables
. 这些参数都不是必须的,但是你至少要使用一个!要注意这些额外的方式对不同的数据库引擎可能存在移植性问题.(因为你在显式的书写SQL语句),除非万不得已,尽量避免这样。
参数之select
The select
参数可以让你在 SELECT
从句中添加其他字段信息,它应该是一个字典,存放着属性名到 SQL 从句的映射。
queryResult=models.Article
.objects.extra(select={'is_recent': "create_time > '2017-09-05'"})
结果集中每个 Entry 对象都有一个额外的属性is_recent, 它是一个布尔值,表示 Article对象的create_time 是否晚于2017-09-05.
参数之where
/ tables
您可以使用where
定义显式SQL WHERE
子句 - 也许执行非显式连接。您可以使用tables
手动将表添加到SQL FROM
子句。
where
和tables
都接受字符串列表。所有where
参数均为“与”任何其他搜索条件。
举例来讲:
queryResult=models.Article
.objects.extra(where=['nid in (3,4) OR title like "py%" ','nid>2'])
8.7、上传文件
8.7.1、form表单上传文件
<h3>form表单上传文件</h3>
<form action="/upload_file/" method="post" enctype="multipart/form-data">
<p><input type="file" name="upload_file_form"></p>
<input type="submit">
</form>
def index(request):
return render(request,"index.html")
def upload_file(request):
print("FILES:",request.FILES)
print("POST:",request.POST)
return HttpResponse("上传成功!")
8.7.2、Ajax(基于FormData)
FormData是什么呢?
XMLHttpRequest Level 2添加了一个新的接口FormData
.利用FormData对象
,我们可以通过JavaScript用一些键值对来模拟一系列表单控件,我们还可以使用XMLHttpRequest的send()
方法来异步的提交这个"表单".比起普通的ajax,使用FormData
的最大优点就是我们可以异步上传一个二进制文件.
所有主流浏览器的较新版本都已经支持这个对象了,比如Chrome 7+、Firefox 4+、IE 10+、Opera 12+、Safari 5+。
<h3>Ajax上传文件</h3>
<p><input type="text" name="username" id="username" placeholder="username"></p>
<p><input type="file" name="upload_file_ajax" id="upload_file_ajax"></p>
<button id="upload_button">提交</button>
{#注意button标签不要用在form表单中使用#}
<script>
$("#upload_button").click(function(){
var username=$("#username").val();
var upload_file=$("#upload_file_ajax")[0].files[0];
var formData=new FormData();
formData.append("username",username);
formData.append("upload_file_ajax",upload_file);
$.ajax({
url:"/upload_file/",
type:"POST",
data:formData,
contentType:false,
processData:false,
success:function(){
alert("上传成功!")
}
});
})
</script>
def index(request):
return render(request,"index.html")
def upload_file(request):
print("FILES:",request.FILES)
print("POST:",request.POST)
return HttpResponse("上传成功!")
8.7.3、ImageField 和 FileField
ImageField 和 FileField 可以分别对图片和文件进行上传到指定的文件夹中。
- 在下面的 models.py 中 :
picture = models.ImageField(upload_to=‘avatars/’, default=“avatars/default.png”,blank=True, null=True) 注:定义 ImageField 字段时必须制定参数 upload_to这个字段要写相对路径,
这个参数会加在 settings.py 中的 MEDIA_ROOT后面, 形成一个路径, 这个路径就是上 传图片的存放位置,默认在Django项目根路径下,也就是MEDIA_ROOT默认是Django根目录
所以要先设置好 mysite/settings.py中的 settings.py 中的 MEDIA_ROOT
class Userinfo(models.Model):
name = models.CharField(max_length=32)
avatar_img = models.FileField("avatars/")
username = request.POST.get("username")
#获取文件对象
file = request.FILES.get("file")
#插入数据,将图片对象直接赋值给字段
user = Userinfo.objects.create(name=username,avatar_img=file)
Django会在项目的根目录创建avatars文件夹,将上传文件下载到该文件夹中,avatar字段保存的是文件的相对路径。
- 在 mysite/settings.py中 :
MEDIA_ROOT = os.path.join(BASE_DIR,"media")
MEDIA_URL='/media/'
MEDIA_ROOT:存放 media 的路径, 这个值加上 upload_to的值就是真实存放上传图片文件位置
MEDIA_URL:给这个属性设值之后,静态文件的链接前面会加上这个值,如果设置这个值,则UserInfo.avatar.url自动替换成:/media/avatars/default.png,可以在模板中直接调用:。
3.url.py:
from django.views.static import serve
# 添加media 配置
re_path(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
浏览器可以直接访问http://127.0.0.1:8000/media/yuan/avatars/%E7%86%8A%E7%8C%AB.webp,即我们的用户上传文件。
最后再给大家补充一个用户文件夹路径
def user_directory_path(instance, filename):
return os.path.join(instance.name,"avatars", filename)
class Userinfo(models.Model):
name = models.CharField(max_length=32)
avatar_img = models.FileField(upload_to=user_directory_path)
4.FileField 和 ImageFiled 相同。
8.7.4、导入表格批量创建数据
# Create your tests here.
def multi_create(request):
# 将接受到的excel文件转到mysql
# (1)下载文件到服务器
emp_excel = request.FILES.get("emp_excel")
print(emp_excel) # 期末题目(陕西联通).xlsx
print(emp_excel.name) # "期末题目(陕西联通).xlsx"
# 下载文件
with open("files/"+emp_excel.name,"wb") as f:
for line in emp_excel:
f.write(line)
# 读取excel,批量导入到mysql中
import os
from openpyxl import load_workbook
file_path = os.path.join("files",emp_excel.name)
# 加载某一个excel文件
wb = load_workbook(file_path)
# 获取sheet对象
print("wb.sheetnames",wb.sheetnames)
worksheet = wb.worksheets[0]
for row in worksheet.iter_rows(min_row=3):
print(row)
if row[0].value == None:
break
Employee.objects.create(name=row[0].value,
age=row[1].value,
ruzhi_date=row[2].value,
dep=row[3].value,
salary=row[4].value,
)
return redirect("/index/")
8.8、websocket的实现
8.8.1、websocket的实现
http请求的特点:
短链接
基于请求响应
基于TCP
无状态保存
WebSocket
是一种在单个TCP
连接上进行全双工通讯的协议。WebSocket
允许服务端主动向客户端推送数据。在WebSocket
协议中,客户端浏览器和服务器只需要完成一次握手就可以创建持久性的连接,并在浏览器和服务器之间进行双向的数据传输。
在一个HTTP访问周期里,如果要执行一个长时间任务,为了避免浏览器等待,后台必须使用异步动作。与此同时也要满足实时需求,用户提交了任务后可以随时去访问任务详情页面,在这里用户能够实时地看到任务的执行进度。
针对异步任务处理,我们使用了Celery把任务放到后台执行。Celery 是一个基于python开发的分布式异步消息任务队列,通过它可以轻松的实现任务的异步处理,关于它的使用方法《网易乐得RDS设计》也有提到。Celery在处理一个任务的时候,会把这个任务的进度记录在数据库中。
实现任务的后台执行后,下一步就要解决实时地更新进度信息到网页的问题。从上一步可以知道,数据库中已经存在了任务的进度信息,网页直接访问数据库就可以拿到数据。但是数据库中的进度信息更新的速率不固定,如果使用间隔时间比较短的ajax轮询来访问数据库,会产生很多无用请求,造成资源浪费。综合考虑,我们决定使用WebSocket来实现推送任务进度信息的功能。网站是使用Django搭建的,原生的MTV(模型-模板-视图)设计模式只支持Http请求。幸好Django开发团队在过去的几年里也看到实时网络应用的蓬勃发展,发布了Channels,以插件的形式为Django带来了实时能力。下面两张图展示了原生Django与集成Channels的Django。
WebSocket的请求头中重要的字段:
Connection和Upgrade:表示客户端发起的WebSocket请求
Sec-WebSocket-Version:客户端所使用的WebSocket协议版本号,服务端会确认是否支持该版本号
Sec-WebSocket-Key:一个Base64编码值,由浏览器随机生成,用于升级request WebSocket的响应头中重要的字段
HTTP/1.1 101 Swi tching Protocols:切换协议,WebSocket协议通过HTTP协议来建立运输层的TCP连接
Connection和Upgrade:表示服务端发起的WebSocket响应 Sec-WebSocket-Accept:表示服务器接受了客户端的请求,由Sec-WebSocket-Key计算得来
WebSocket协议的优点:
支持双向通信,实时性更强 数据格式比较轻量,性能开销小,通信高效 支持扩展,用户可以扩展协议或者实现自定义的子协议(比如支持自定义压缩算法等)
WebSocket协议的缺点:
少部分浏览器不支持,浏览器支持的程度与方式有区别 长连接对后端处理业务的代码稳定性要求更高,后端推送功能相对复杂 成熟的HTTP生态下有大量的组件可以复用,WebSocket较少
WebSocket的应用场景:
即时聊天通信,网站消息通知 在线协同编辑,如腾讯文档 多玩家在线游戏,视频弹幕,股票基金实施报价
8.8.2、Channels组件
Channels为Django带来了一些新特性,最明显的就是添加了对WebSocket的支持。Channels最重要的部分也可以看做是任务队列,消息被生产者推到通道,然后传递给监听通道的消费者之一。它与传统的任务队列的主要的区别在于Channels通过网络工作,使生产者和消费者透明地运行在多台机器上,这个网络层就叫做channel layer。Channels推荐使用redis作为它的channel layer backend,但也可以使用其它类型的工具,例如rabbitmq、内存或者IPC。关于Channels的一些基本概念,推荐阅读官方文档。
继承WebSocketConsumer
的连接。
AuthMiddlewareStack:用于WebSocket认证,继承了Cookie Middleware,SessionMiddleware,SessionMiddleware。django的channels封装了django的auth模块,使用这个配置我们就可以在consumer中通过下边的代码获取到用户的信息
def connect(self):
self.user = self.scope["user"]
self.scope类似于django中的request,包含了请求的type、path、header、cookie、session、user等等有用的信息
配置和使用
channels==2.1.3
channels-redis==2.3.0。
启动 Redis 服务默认使用 6379 端口,Django 将使用该端口连接 Redis 服务。
更新项目配置文件 settings.py 中的 INSTALLED_APPS 项
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app01.apps.App01Config',
'channels',
]
ASGI_APPLICATION = "chat.routing.application"
# WebSocket
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)],
},
},
}
from django.urls import path
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from table.consumers import TableConsumer
application = ProtocolTypeRouter({
# Empty for now (http->django views is added by default)
'websocket': AuthMiddlewareStack(
URLRouter([
path('ws/table/<slug:table_id>/', TableConsumer),
])
),
})
routing.py
路由文件跟django
的url.py
功能类似,语法也一样,意思就是访问ws/table/
都交给TableConsumer处理。
新增 app 名为 table
,在 table
目录下新增 consumers.py
:
from channels.generic.websocket import AsyncJsonWebsocketConsumer
class TableConsumer(AsyncJsonWebsocketConsumer):
table = None
async def connect(self):
self.table = 'table_{}'.format(self.scope['url_route']['kwargs']['table_id'])
# Join room group
await self.channel_layer.group_add(self.table, self.channel_name)
await self.accept()
async def disconnect(self, close_code):
# Leave room group
await self.channel_layer.group_discard(self.table, self.channel_name)
# Receive message from WebSocket
async def receive_json(self, content, **kwargs):
# Send message to room group
self.from_client = str(self.scope["client"])
await self.channel_layer.group_send(self.table, {'type': 'message', 'message': content,"from_client":self.from_client})
# Receive message from room group
async def message(self, event):
message = event['message']
print("self.scope",self.scope)
print("event",event)
print("message",message)
# Send message to WebSocket
print(":::",event["from_client"]+">>>"+message)
await self.send_json(event["from_client"]+">>>"+message)
TableConsumer
类中的函数依次用于处理连接、断开连接、接收消息和处理对应类型的消息,其中channel_layer.group_send(self.table, {'type': 'message', 'message': content})
方法,self.table
参数为当前组的组id, {'type': 'message', 'message': content}
部分分为两部分,type
用于指定该消息的类型,根据消息类型调用不同的函数去处理消息,而 message
内为消息主体。
在 table
目录下的 views.py
中新增函数:
from django.shortcuts import render
def table(request, table_id):
return render(request, 'table.html', {
'room_name_json': table_id
})
from django.contrib import admin
from django.urls import path, re_path
from table.views import table
urlpatterns = [
path('admin/', admin.site.urls),
re_path('table/(\d+)', table),
]
前端实现WebSocket
WebSocket对象一个支持四个消息:onopen,onmessage,oncluse和onerror,我们这里用了两个onmessage和onclose
onopen: 当浏览器和websocket服务端连接成功后会触发onopen消息
onerror: 如果连接失败,或者发送、接收数据失败,或者数据处理出错都会触发onerror消息
onmessage: 当浏览器接收到websocket服务器发送过来的数据时,就会触发onmessage消息,参数e包含了服务端发送过来的数据
onclose: 当浏览器接收到websocket服务器发送过来的关闭连接请求时,会触发onclose消息载请注明出处。
在 table
的 templates\table
目录下新增 table.html
:
<!-- chat/templates/chat/room.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Chat Room</title>
</head>
<body>
<textarea id="chat-log" cols="100" rows="20"></textarea><br/>
<input id="chat-message-input" type="text" size="100"/><br/>
<input id="chat-message-submit" type="button" value="Send"/>
</body>
<script>
var roomName = {{ room_name_json }};
var chatSocket = new WebSocket('ws://' + window.location.host + '/ws/table/' + roomName + '/');
chatSocket.onmessage = function (e) {
var data = JSON.parse(e.data);
document.querySelector('#chat-log').value += (JSON.stringify(data).slice(1,-1) + '\n');
};
chatSocket.onclose = function (e) {
console.error('Chat socket closed unexpectedly');
};
document.querySelector('#chat-message-input').focus();
document.querySelector('#chat-message-input').onkeyup = function (e) {
if (e.keyCode === 13) { // enter, return
document.querySelector('#chat-message-submit').click();
}
};
document.querySelector('#chat-message-submit').onclick = function (e) {
var messageInputDom = document.querySelector('#chat-message-input');
var message = messageInputDom.value;
chatSocket.send(JSON.stringify(message));
messageInputDom.value = '';
};
</script>
</html>