rayzz

rayzz 查看完整档案

杭州编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

rayzz 赞了文章 · 2019-01-16

flask入门1

flask

每天的内容

  1. flask跑起来
  2. 模板引擎
  3. flask表单
  4. 文件上传邮件发送
  5. flask-sqlalchemy

一、web框架的简介

M 模型 负责数据的操作

V 视图 负责数据的展示

C 控制器 控制你的M的操作以及视图模板的渲染

在python中叫做MVT

M 模型 负责数据的操作

V 控制你的M的操作以及视图模板的渲染 业务逻辑的操作

T templates 模板 负责数据的展示

二、架构

BS browser-》server

CS client-》server

三、FLASK

概念: flask是一个非常小的web框架 被称为微型框架 只提供了一个强健的核心 其它的都是通过第三方扩展库来实现

组成

  1. 调试 路由 WSGI
  2. 模板引擎 jinja2 (就是由flask核心人员开发的模板引擎)

使用:

安装 pip install flask

实例
from flask import Flask

app = Flask(__name__) #实例化flask

#路由地址 根据用户不同的url进行处理
@app.route('/')
def index():#处理当前请求的函数
    return 'Hello Flask'

if __name__ == '__main__':
    app.run() #运行当前的flask

四、视图函数

(1) 无参路由

#路由地址和视图函数名称 是否同名没有关系
#http://127.0.0.1:5000/test/
@app.route('/test/') #路由地址末尾的/建议加上
def test():
    return '我是测试使用的视图函数'

(2) 带一个参数的路由

http://127.0.0.1:5000/page/10/

@app.route('/page/<pagenum>/') #参数的语法格式 /路由名称/<形参名>/
def page(pagenum):
    return '当前的页码为{}'.format(pagenum)

(3) 带多个参数

#带多个参数
# http://127.0.0.1:5000/arg/10/zhansgan/
@app.route('/arg/<age>/<name>/')
def getarg(age,name):
    return '我叫{} 我见年{}岁了'.format(name,age)
# http://127.0.0.1:5000/arg/zhansgan_10/
@app.route('/arg/<name>_<age>/')
def getarg(age,name):
    return '我叫{} 我见年{}岁了'.format(name,age)

(4) 限制参数的类型

#参数类型
# @app.route('/argtype/<arg>/')
# @app.route('/argtype/<int:arg>/') #限定参数类型为int
# @app.route('/argtype/<float:arg>/') #限定参数类型为float
# @app.route('/argtype/<string:arg>/') #限定参数类型为string 默认就是字符串
@app.route('/argtype/<path:arg>/') #其实path就是string 但是path会将路由地址后面的所有的路由或者值都认为是一个值 /不在作为分隔符来使用
def argtype(arg):
    print(type(arg))
    print(arg)
    return '获取参数类型的视图函数'

注意

  1. 路由地址末尾的/建议加上 因为如果输入的时候没有加默认的/浏览器会自动帮你加上
  2. 形参名字写在路由的<> 中间
  3. 参数默认类型都为string

五、视图函数的响应

(1) return 字符串进行响应

@app.route('/response/')
def res():
    return '我是响应',404 #响应一个指定标准的状态码

(2) 通过make_response构造响应

导入:

from flask import make_response

@app.route('/make_response/')
def makeResponse():
    res = make_response('我是响应的内容')
    # res = make_response('我是响应的内容',404)
    return res

六、重定向 redirect

作用: 从一个地址跳向另外一个地址

导入

from flask import redirect

实例

@app.route('/')
def index():#处理当前请求的函数
    return 'Hello Flask'

#重定向
@app.route('/redirect/')
def redirect_index():
    return redirect('/') #参数为路由地址
    return redirect('/argtype/redirect_index/') #带参数路由地址的重定向

url_for 通过视图函数名称 反向构造出路由地址

导入

from flask import redirect,url_for

实例

@app.route('/redirect/')
def redirect_index():
    url = url_for('test')
    url = url_for('getarg',name='zhangsan',age=18) #带多个参数
      #@app.route('/arg/<name>_<age>/')
      #def getarg(age,name):
    return url #/test/

注意:

如果给定的视图函数名称不存在 则抛出异常

url_for 和 redirect 组合使用

@app.route('/redirect/')
def redirect_index():
    return redirect(url_for('test'))
    return redirect(url_for('getarg',name='zhangsan',age=18)) #带多个参数

七、abort 终止

概念:

在视图函数中处理的时候 可以使用abort抛出指定状态码的错误 下面代码不在执行

需要抛出标准http的状态码

from flask import abort

实例

@app.route('/abort/')
def my_abort():
    # abort(404)
    # abort(500)
    return '抛出状态码'
#捕获500的错误
@app.errorhandler(500)
def server_error(e):
    return '现在能看到了吗{}'.format(e)

#捕获404的错误信息
@app.errorhandler(404)
def server_error(e):
    return '您访问的页面被外星人劫持走了!!!'

八、app.run() 参数说明

参数参数说明默认值
host主机名127.0.0.1
port端口号5000
debug调试False
threaded多线程False

实例

if __name__ == '__main__':
    # app.run(debug=True) #开启调试模式
    app.run(host='0.0.0.0',port=5001,debug=True,threaded=True)

十、请求 request

说明:

request是由flask框架为我们提供好的对象 使用时 只要导入即可

用户在请求的时候 框架会为当前请求的用户 创建一个request(请求的对象) 包含当前用户请求的所有信息

导入

from flask import request

  1. url 用户请求的完整的url
  2. base_url 去除get传参后的url
  3. host_url 只有主机和端口号的url
  4. path 获取请求的路由地址
  5. method 请求的方法
  6. args 获取get传参
  7. form 获取表单传递过来的数据
  8. files 获取文件上传过来的数据
  9. headers 获取用户请求过来的头信息
  10. cookies 获取用户请求过来的所有cookie
  11. json 获取用户请求过来的json数据

实例

@app.route('/request/')
def get_request():
    print('用户请求的完整的url',request.url)
    print('去除get传参后的url',request.base_url)
    print('只有主机和端口号的url',request.host_url)
    print('获取请求的路由地址',request.path)
    print(' 请求的方法',request.method)
    print('获取拼凑的get传参',request.args)
    print('获取拼凑的get传参',request.args.get('name'))
    print('获取拼凑的get传参',request.args.get('age'))
    print('获取表单传递过来的数据',request.form)
    print('获取文件上传过来的数据',request.files)
    print('获取用户请求过来的头信息',request.headers)
    print('获取用户请求过来的所有cookie',request.cookies)
    print('获取用户请求过来的json数据',request.json)
    return 'request对象'

十一、会话控制 cookie和session

cookie

设置cookie

response.set_cookie(
    key,  #设置键
    value,#设置值
    max_age=None, #过期时间
    path = '/' #当前cookie的存储路径
)

获取cookie

@app.route('/get_cookie/')
def get_cookie():
    print(request.cookies)
    return request.cookies.get('name','default默认值')

删除cookie

#清除cookie
@app.route('/del_cookie/')
def del_cookie():
    res = make_response('清除cookie')
    res.delete_cookie('name')
    return res

cookie存储值为明文存储 安全性低

cookie存在客户端(浏览器中)

cookie默认存活时间为 当前浏览结束(关闭当前的浏览器)

session

session的使用 需要一个secret_key 来进行加密产生加密的字符串

app.config['SECRET_KEY'] = 'secretkey'

会给cookie设置一个唯一的标识符 sessionId 服务器端会通过cookie携带着唯一的sessionId来区分是哪一个用户的请求 如果客户端的cookie被禁用了 那么服务器端的session将无法使用 session基于cookie

设置session

#设置session
@app.route('/set_session/')
def set_session():
     默认存活当前浏览器结束
    session['username'] = '张三'
    return '设置session'

设置session 及过期时间

#设置session
@app.route('/set_session/')
def set_session():
    session.permanent = True #设置session持久化存储
    #设置当前session的存活时间60秒 如果当前设置失败 那么存活时间为1月
    app.permanent_session_lifetime = timedelta(seconds=60)
    session['username'] = '张三'
    return '设置session'

获取session

#获取session
@app.route('/get_session/')
def get_session():
    return session.get('username','default默认值')

删除session

@app.route('/del_session/')
def del_session():
    #删除 key为username的session
    session.pop('username')
    #删除所有session
    # session.clear()
    return '删除session'

十二、flask-script扩展

简介:

就是一个flask终端运行的解析器 通过不同参数 来设置flask的启动项

安装

sudo pip3 install flask-script

使用

from flask_script import Manager #导入终端运行的解析器
app = Flask(__name__)
manager = Manager(app)
...
if __name__ == '__main__':
    manager.run()

启动参数

-h主机
-p端口号
-d调试
-r重新加载
-threaded多线程
python manage.py runserver -h

python manage.py runserver -h0.0.0.0 -p5000 -d -r --threaded

python manage.py runserver -d -r

十三、蓝本蓝图 Blueprint

概述

当所有代码越爱越多的时候 在manage.py中 很明显是不合理的 我们需要将不同功能的视图函数 存放在不同的文件中 使用我们的项目的目录结构更加的清晰

使用

user.py 用户的处理
from flask import Blueprint

user = Blueprint('user',__name__)

@user.route('/login/')
def login():
    return '登录'
manage.py中
from mysession import mysession
from user import user
#http://127.0.0.1:5000/login/
app.register_blueprint(user) #注册蓝本
#http://127.0.0.1:5000/user/login/
app.register_blueprint(user,url_prefix='/user') #注册蓝本并添加前缀

蓝本中的重定向

@app.route('/')
def index():
    # return '首页'
    return redirect('/user/login/')
    return redirect(url_for('user.login')) #使用url_for反向构造出路由的时候 需要指定当前的视图函数 是哪一个蓝本对象的

十四、请求钩子函数

在manage文件中使用

钩子函数功能描述
before_first_request第一次请求之前
before_request每次请求之前
after_request每次请求之后 没有异常
teardown_request每次请求之后 即使有异常出现

实例

@app.before_first_request
def before_first_request():
    print('before_first_request')

@app.before_request
def before_request():
    print('before_request')
    if request.method == 'GET' and request.path == '/form/':
        abort(500)

@app.after_request
def before_request(r):
    print('before_request',r)
    return r

@app.teardown_request
def teardown_request(r):
    print('teardown_request')
    return r
在蓝本中使用
钩子函数功能描述
before_app_first_request第一次请求之前
before_app_request每次请求之前
after_app_request每次请求之后 没有异常
teardown_app_request每次请求之后 即使有异常出现

实例

@user.before_app_first_request
def before_first_request():
    print('before_first_request')

@user.before_app_request
def before_request():
    print('before_request')
    if request.method == 'GET' and request.path == '/form/':
        abort(500)

@user.after_app_request
def after_request(r):
    print('after_request',r)
    return r

@user.teardown_app_request
def teardown_request(r):
    print('teardown_request')
    return r

注意:

钩子函数写在蓝本或者启动文件中 都可以捕获到所有的请求和响应(一样)一个flask中只需要写一个钩子函数而不需要重复写钩子函数

flask入门2-模板引擎

查看原文

赞 4 收藏 4 评论 0

rayzz 发布了文章 · 2019-01-09

linux下typora安装

# optional, but recommended
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys BA300B7755AFCFAE

# add Typora's repository
sudo add-apt-repository 'deb https://typora.io ./linux/'
sudo apt-get update

# install typora
sudo apt-get install typora
查看原文

赞 0 收藏 0 评论 0

rayzz 发布了文章 · 2019-01-09

python 读取excel文件并写入json

excel内容:
图片描述
代码:

import xlrd
import json
import operator
 
 
def read_xlsx(filename):
    # 打开excel文件
    data1 = xlrd.open_workbook(filename)
    # 读取第一个工作表
    table = data1.sheets()[0]
    # 统计行数
    n_rows = table.nrows
 
    data = []
 
    # 微信文章属性:wechat_name wechat_id title abstract url time read like number
    for v in range(1, n_rows-1):
        # 每一行数据形成一个列表
        values = table.row_values(v)
        # 列表形成字典
        data.append({'wechat_name': values[0],
                     'wechat_id':   values[1],
                     'title':       values[2],
                     'abstract':    values[3],
                     'url':         values[4],
                     'time':        values[5],
                     'read':        values[6],
                     'like':        values[7],
                     'number':      values[8],
                     })
    # 返回所有数据
    return data
 
 
if __name__ == '__main__':
    d = []
    # 循环打开每个excel
    for i in range(1, 16):
        d1 = read_xlsx('./excel data/'+str(i)+'.xlsx')
        d.extend(d1)
 
    # 微信文章属性
    # 按时间升序排列
    d = sorted(d, key=operator.itemgetter('time'))
    # 写入json文件
    with open('article.json', 'w', encoding='utf-8') as f:
        #ensure_ascii=False显示中文,indent=2缩进为2
        f.write(json.dumps(d, ensure_ascii=False, indent=2))
 
    name = []
    # 微信id写文件
    f1 = open('wechat_id.txt', 'w')
    for i in d:
        if i['wechat_id'] not in name:
            name.append(i['wechat_id'])
        f1.writelines(i['wechat_id'])
        f1.writelines('\n')
 
    print(len(name))
查看原文

赞 1 收藏 0 评论 0

rayzz 赞了回答 · 2018-10-16

解决普通用户没有权限使用pip,怎么解决?

要么使用sudo
要么virtualenv建立一个你自己的python环境

python教程

关注 1 回答 1

rayzz 发布了文章 · 2018-09-18

linux下typora安装

# optional, but recommended
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys BA300B7755AFCFAE

# add Typora's repository
sudo add-apt-repository 'deb https://typora.io ./linux/'
sudo apt-get update

# install typora
sudo apt-get install typora
查看原文

赞 0 收藏 0 评论 0

rayzz 发布了文章 · 2018-09-17

python 读取excel文件并写入json

excel内容:
图片描述
代码:

import xlrd
import json
import operator
 
 
def read_xlsx(filename):
    # 打开excel文件
    data1 = xlrd.open_workbook(filename)
    # 读取第一个工作表
    table = data1.sheets()[0]
    # 统计行数
    n_rows = table.nrows
 
    data = []
 
    # 微信文章属性:wechat_name wechat_id title abstract url time read like number
    for v in range(1, n_rows-1):
        # 每一行数据形成一个列表
        values = table.row_values(v)
        # 列表形成字典
        data.append({'wechat_name': values[0],
                     'wechat_id':   values[1],
                     'title':       values[2],
                     'abstract':    values[3],
                     'url':         values[4],
                     'time':        values[5],
                     'read':        values[6],
                     'like':        values[7],
                     'number':      values[8],
                     })
    # 返回所有数据
    return data
 
 
if __name__ == '__main__':
    d = []
    # 循环打开每个excel
    for i in range(1, 16):
        d1 = read_xlsx('./excel data/'+str(i)+'.xlsx')
        d.extend(d1)
 
    # 微信文章属性
    # 按时间升序排列
    d = sorted(d, key=operator.itemgetter('time'))
    # 写入json文件
    with open('article.json', 'w', encoding='utf-8') as f:
        #ensure_ascii=False显示中文,indent=2缩进为2
        f.write(json.dumps(d, ensure_ascii=False, indent=2))
 
    name = []
    # 微信id写文件
    f1 = open('wechat_id.txt', 'w')
    for i in d:
        if i['wechat_id'] not in name:
            name.append(i['wechat_id'])
        f1.writelines(i['wechat_id'])
        f1.writelines('\n')
 
    print(len(name))
查看原文

赞 1 收藏 0 评论 0

rayzz 发布了文章 · 2018-08-25

Django的 select_related 和 prefetch_related 函数对 QuerySet 查询的优化

在数据库有外键的时候,使用 select_related() 和 prefetch_related() 可以很好的减少数据库请求的次数,从而提高性能。本文通过一个简单的例子详解这两个函数的作用。虽然QuerySet的文档中已经详细说明了,但本文试图从QuerySet触发的SQL语句来分析工作方式,从而进一步了解Django具体的运作方式。

1. 实例的背景说明

假定一个个人信息系统,需要记录系统中各个人的故乡、居住地、以及到过的城市。数据库设计如下:

20140804002519328

Models.py 内容如下:

from django.db import models

class Province(models.Model):
    name = models.CharField(max_length=10)
    def __unicode__(self):
        return self.name

class City(models.Model):
    name = models.CharField(max_length=5)
    province = models.ForeignKey(Province)
    def __unicode__(self):
        return self.name

class Person(models.Model):
    firstname  = models.CharField(max_length=10)
    lastname   = models.CharField(max_length=10)
    visitation = models.ManyToManyField(City, related_name = "visitor")
    hometown   = models.ForeignKey(City, related_name = "birth")
    living     = models.ForeignKey(City, related_name = "citizen")
    def __unicode__(self):
        return self.firstname + self.lastname

注1:创建的app名为“QSOptimize”

注2:为了简化起见,qsoptimize_province 表中只有2条数据:湖北省和广东省,qsoptimize_city表中只有三条数据:武汉市、十堰市和广州市

2. select_related()

对于一对一字段(OneToOneField)和外键字段(ForeignKey),可以使用select_related 来对QuerySet进行优化

作用和方法

在对QuerySet使用select_related()函数后,Django会获取相应外键对应的对象,从而在之后需要的时候不必再查询数据库了。以上例说明,如果我们需要打印数据库中的所有市及其所属省份,最直接的做法是:

>>> citys = City.objects.all()
>>> for c in citys:
...   print c.province
...

这样会导致线性的SQL查询,如果对象数量n太多,每个对象中有k个外键字段的话,就会导致n*k+1次SQL查询。在本例中,因为有3个city对象就导致了4次SQL查询:

SELECT `QSOptimize_city`.`id`, `QSOptimize_city`.`name`, `QSOptimize_city`.`province_id`
FROM `QSOptimize_city`
 
SELECT `QSOptimize_province`.`id`, `QSOptimize_province`.`name` 
FROM `QSOptimize_province`
WHERE `QSOptimize_province`.`id` = 1 ;
 
SELECT `QSOptimize_province`.`id`, `QSOptimize_province`.`name` 
FROM `QSOptimize_province`
WHERE `QSOptimize_province`.`id` = 2 ;
 
SELECT `QSOptimize_province`.`id`, `QSOptimize_province`.`name` 
FROM `QSOptimize_province`
WHERE `QSOptimize_province`.`id` = 1 ;

注:这里的SQL语句是直接从Django的logger:‘django.db.backends’输出出来的

如果我们使用select_related()函数:

>>> citys = City.objects.select_related().all()
>>> for c in citys:
...   print c.province
...

就只有一次SQL查询,显然大大减少了SQL查询的次数:

SELECT `QSOptimize_city`.`id`, `QSOptimize_city`.`name`, 
`QSOptimize_city`.`province_id`, `QSOptimize_province`.`id`, `QSOptimize_province`.`name` 
FROM`QSOptimize_city` 
INNER JOIN `QSOptimize_province` ON (`QSOptimize_city`.`province_id` = `QSOptimize_province`.`id`) ;

这里我们可以看到,Django使用了INNER JOIN来获得省份的信息。顺便一提这条SQL查询得到的结果如下:

+----+-----------+-------------+----+-----------+
| id | name      | province_id | id | name      |
+----+-----------+-------------+----+-----------+
|  1 | 武汉市    |           1 |  1 | 湖北省    |
|  2 | 广州市    |           2 |  2 | 广东省    |
|  3 | 十堰市    |           1 |  1 | 湖北省    |
+----+-----------+-------------+----+-----------+
3 rows in set (0.00 sec)

使用方法

函数支持如下三种用法:

*fields 参数

select_related() 接受可变长参数,每个参数是需要获取的外键(父表的内容)的字段名,以及外键的外键的字段名、外键的外键的外键…。若要选择外键的外键需要使用两个下划线“__”来连接。

例如我们要获得张三的现居省份,可以用如下方式:

>>> zhangs = Person.objects.select_related('living__province').get(firstname=u"张",lastname=u"三")
>>> zhangs.living.province

触发的SQL查询如下:

SELECT `QSOptimize_person`.`id`, `QSOptimize_person`.`firstname`, 
`QSOptimize_person`.`lastname`, `QSOptimize_person`.`hometown_id`, `QSOptimize_person`.`living_id`, 
`QSOptimize_city`.`id`, `QSOptimize_city`.`name`, `QSOptimize_city`.`province_id`, `QSOptimize_province`.`id`, 
`QSOptimize_province`.`name` 
FROM `QSOptimize_person` 
INNER JOIN `QSOptimize_city` ON (`QSOptimize_person`.`living_id` = `QSOptimize_city`.`id`) 
INNER JOIN `QSOptimize_province` ON (`QSOptimize_city`.`province_id` = `QSOptimize_province`.`id`) 
WHERE (`QSOptimize_person`.`lastname` = '三'  AND `QSOptimize_person`.`firstname` = '张' );

可以看到,Django使用了2次 INNER JOIN 来完成请求,获得了city表和province表的内容并添加到结果表的相应列,这样在调用 zhangs.living的时候也不必再次进行SQL查询。

+----+-----------+----------+-------------+-----------+----+-----------+-------------+----+-----------+
| id | firstname | lastname | hometown_id | living_id | id | name      | province_id | id | name      |
+----+-----------+----------+-------------+-----------+----+-----------+-------------+----+-----------+
|  1 | 张        | 三       |           3 |         1 |  1 | 武汉市    |   1         |  1 | 湖北省    |
+----+-----------+----------+-------------+-----------+----+-----------+-------------+----+-----------+
1 row in set (0.00 sec)

然而,未指定的外键则不会被添加到结果中。这时候如果需要获取张三的故乡就会进行SQL查询了:

>>> zhangs.hometown.province
SELECT `QSOptimize_city`.`id`, `QSOptimize_city`.`name`, 
`QSOptimize_city`.`province_id` 
FROM `QSOptimize_city` 
WHERE `QSOptimize_city`.`id` = 3 ;

SELECT `QSOptimize_province`.`id`, `QSOptimize_province`.`name` 
FROM `QSOptimize_province` 
WHERE `QSOptimize_province`.`id` = 1

同时,如果不指定外键,就会进行两次查询。如果深度更深,查询的次数更多。

值得一提的是,从Django 1.7开始,select_related()函数的作用方式改变了。在本例中,如果要同时获得张三的故乡和现居地的省份,在1.7以前你只能这样做:

>>> zhangs = Person.objects.select_related('hometown__province','living__province').get(firstname=u"张",lastname=u"三")
>>> zhangs.hometown.province
>>> zhangs.living.province

但是1.7及以上版本,你可以像和queryset的其他函数一样进行链式操作:

>>> zhangs = Person.objects.select_related('hometown__province').select_related('living__province').get(firstname=u"张",lastname=u"三")
>>> zhangs.hometown.province
>>> zhangs.living.province

如果你在1.7以下版本这样做了,你只会获得最后一个操作的结果,在本例中就是只有现居地而没有故乡。在你打印故乡省份的时候就会造成两次SQL查询。

depth 参数

select_related() 接受depth参数,depth参数可以确定select_related的深度。Django会递归遍历指定深度内的所有的OneToOneField和ForeignKey。以本例说明:

>>> zhangs = Person.objects.select_related(depth = d)

d=1  相当于 select_related(‘hometown’,’living’)

d=2  相当于 select_related(‘hometown__province’,’living__province’)

无参数

select_related() 也可以不加参数,这样表示要求Django尽可能深的select_related。例如:zhangs = Person.objects.select_related().get(firstname=u”张”,lastname=u”三”)。但要注意两点:

  1. Django本身内置一个上限,对于特别复杂的表关系,Django可能在你不知道的某处跳出递归,从而与你想的做法不一样。具体限制是怎么工作的我表示不清楚。
  2. Django并不知道你实际要用的字段有哪些,所以会把所有的字段都抓进来,从而会造成不必要的浪费而影响性能。 

小结

  1. select_related主要针一对一和多对一关系进行优化。
  2. select_related使用SQL的JOIN语句进行优化,通过减少SQL查询的次数来进行优化、提高性能。
  3. 可以通过可变长参数指定需要select_related的字段名。也可以通过使用双下划线“__”连接字段名来实现指定的递归查询。没有指定的字段不会缓存,没有指定的深度不会缓存,如果要访问的话Django会再次进行SQL查询。
  4. 也可以通过depth参数指定递归的深度,Django会自动缓存指定深度内所有的字段。如果要访问指定深度外的字段,Django会再次进行SQL查询。
  5. 也接受无参数的调用,Django会尽可能深的递归查询所有的字段。但注意有Django递归的限制和性能的浪费。
  6. Django >= 1.7,链式调用的select_related相当于使用可变长参数。Django < 1.7,链式调用会导致前边的select_related失效,只保留最后一个。

3. prefetch_related()

对于多对多字段(ManyToManyField)和一对多字段,可以使用prefetch_related()来进行优化。或许你会说,没有一个叫OneToManyField的东西啊。实际上 ,ForeignKey就是一个多对一的字段,而被ForeignKey关联的字段就是一对多字段了。

作用和方法

prefetch_related()和select_related()的设计目的很相似,都是为了减少SQL查询的数量,但是实现的方式不一样。后者是通过JOIN语句,在SQL查询内解决问题。但是对于多对多关系,使用SQL语句解决就显得有些不太明智,因为JOIN得到的表将会很长,会导致SQL语句运行时间的增加和内存占用的增加。若有n个对象,每个对象的多对多字段对应Mi条,就会生成Σ(n)Mi 行的结果表。prefetch_related()的解决方法是,分别查询每个表,然后用Python处理他们之间的关系。继续以上边的例子进行说明,如果我们要获得张三所有去过的城市,使用prefetch_related()应该是这么做:

>>> zhangs = Person.objects.prefetch_related('visitation').get(firstname=u"张",lastname=u"三")
>>> for city in zhangs.visitation.all() :
...   print city
...

上述代码触发的SQL查询如下:

SELECT `QSOptimize_person`.`id`, `QSOptimize_person`.`firstname`,
`QSOptimize_person`.`lastname`, `QSOptimize_person`.`hometown_id`, `QSOptimize_person`.`living_id`
FROM `QSOptimize_person`
WHERE (`QSOptimize_person`.`lastname` = '三'  AND `QSOptimize_person`.`firstname` = '张');
 
SELECT (`QSOptimize_person_visitation`.`person_id`) AS `_prefetch_related_val`, `QSOptimize_city`.`id`,
`QSOptimize_city`.`name`, `QSOptimize_city`.`province_id`
FROM `QSOptimize_city`
INNER JOIN `QSOptimize_person_visitation` ON (`QSOptimize_city`.`id` = `QSOptimize_person_visitation`.`city_id`)
WHERE `QSOptimize_person_visitation`.`person_id` IN (1);

第一条SQL查询仅仅是获取张三的Person对象,第二条比较关键,它选取关系表QSOptimize_person_visitationperson_id为张三的行,然后和city表内联(INNER JOIN 也叫等值连接)得到结果表。

+----+-----------+----------+-------------+-----------+
| id | firstname | lastname | hometown_id | living_id |
+----+-----------+----------+-------------+-----------+
|  1 | 张        | 三       |           3 |         1 |
+----+-----------+----------+-------------+-----------+
1 row in set (0.00 sec)
 
+-----------------------+----+-----------+-------------+
| _prefetch_related_val | id | name      | province_id |
+-----------------------+----+-----------+-------------+
|                     1 |  1 | 武汉市    |           1 |
|                     1 |  2 | 广州市    |           2 |
|                     1 |  3 | 十堰市    |           1 |
+-----------------------+----+-----------+-------------+
3 rows in set (0.00 sec)

显然张三武汉、广州、十堰都去过。

又或者,我们要获得湖北的所有城市名,可以这样:

>>> hb = Province.objects.prefetch_related('city_set').get(name__iexact=u"湖北省")
>>> for city in hb.city_set.all():
...   city.name
...

触发的SQL查询:

SELECT `QSOptimize_province`.`id`, `QSOptimize_province`.`name` 
FROM `QSOptimize_province` 
WHERE `QSOptimize_province`.`name` LIKE '湖北省' ;

SELECT `QSOptimize_city`.`id`, `QSOptimize_city`.`name`, `QSOptimize_city`.`province_id` 
FROM `QSOptimize_city` 
WHERE `QSOptimize_city`.`province_id` IN (1);

得到的表:

+----+-----------+
| id | name      |
+----+-----------+
|  1 | 湖北省    |
+----+-----------+
1 row in set (0.00 sec)

+----+-----------+-------------+
| id | name      | province_id |
+----+-----------+-------------+
|  1 | 武汉市    |           1 |
|  3 | 十堰市    |           1 |
+----+-----------+-------------+
2 rows in set (0.00 sec)

我们可以看见,prefetch使用的是 IN 语句实现的。这样,在QuerySet中的对象数量过多的时候,根据数据库特性的不同有可能造成性能问题。

使用方法

*lookups 参数

prefetch_related()在Django < 1.7 只有这一种用法。和select_related()一样,prefetch_related()也支持深度查询,例如要获得所有姓张的人去过的省:

>>> zhangs = Person.objects.prefetch_related('visitation__province').filter(firstname__iexact=u'张')
>>> for i in zhangs:
...   for city in i.visitation.all():
...     print city.province
...

触发的SQL:

SELECT `QSOptimize_person`.`id`, `QSOptimize_person`.`firstname`, 
`QSOptimize_person`.`lastname`, `QSOptimize_person`.`hometown_id`, `QSOptimize_person`.`living_id` 
FROM `QSOptimize_person` 
WHERE `QSOptimize_person`.`firstname` LIKE '张' ;

SELECT (`QSOptimize_person_visitation`.`person_id`) AS `_prefetch_related_val`, `QSOptimize_city`.`id`,
`QSOptimize_city`.`name`, `QSOptimize_city`.`province_id` FROM `QSOptimize_city` 
INNER JOIN `QSOptimize_person_visitation` ON (`QSOptimize_city`.`id` = `QSOptimize_person_visitation`.`city_id`)
WHERE `QSOptimize_person_visitation`.`person_id` IN (1, 4);

SELECT `QSOptimize_province`.`id`, `QSOptimize_province`.`name` 
FROM `QSOptimize_province` 
WHERE `QSOptimize_province`.`id` IN (1, 2);

获得的结果:

+----+-----------+----------+-------------+-----------+
| id | firstname | lastname | hometown_id | living_id |
+----+-----------+----------+-------------+-----------+
|  1 | 张        | 三       |           3 |         1 |
|  4 | 张        | 六       |           2 |         2 |
+----+-----------+----------+-------------+-----------+
2 rows in set (0.00 sec)

+-----------------------+----+-----------+-------------+
| _prefetch_related_val | id | name      | province_id |
+-----------------------+----+-----------+-------------+
|                     1 |  1 | 武汉市    |           1 |
|                     1 |  2 | 广州市    |           2 |
|                     4 |  2 | 广州市    |           2 |
|                     1 |  3 | 十堰市    |           1 |
+-----------------------+----+-----------+-------------+
4 rows in set (0.00 sec)

+----+-----------+
| id | name      |
+----+-----------+
|  1 | 湖北省    |
|  2 | 广东省    |
+----+-----------+
2 rows in set (0.00 sec)

值得一提的是,链式prefetch_related会将这些查询添加起来,就像1.7中的select_related那样。 

要注意的是,在使用QuerySet的时候,一旦在链式操作中改变了数据库请求,之前用prefetch_related缓存的数据将会被忽略掉。这会导致Django重新请求数据库来获得相应的数据,从而造成性能问题。这里提到的改变数据库请求指各种filter()、exclude()等等最终会改变SQL代码的操作。而all()并不会改变最终的数据库请求,因此是不会导致重新请求数据库的。

举个例子,要获取所有人访问过的城市中带有“市”字的城市,这样做会导致大量的SQL查询:

plist = Person.objects.prefetch_related('visitation')
[p.visitation.filter(name__icontains=u"市") for p in plist]

因为数据库中有4人,导致了2+4次SQL查询:

SELECT `QSOptimize_person`.`id`, `QSOptimize_person`.`firstname`, `QSOptimize_person`.`lastname`, 
`QSOptimize_person`.`hometown_id`, `QSOptimize_person`.`living_id` 
FROM `QSOptimize_person`;

SELECT (`QSOptimize_person_visitation`.`person_id`) AS `_prefetch_related_val`, `QSOptimize_city`.`id`,
`QSOptimize_city`.`name`, `QSOptimize_city`.`province_id` 
FROM `QSOptimize_city` 
INNER JOIN `QSOptimize_person_visitation` ON (`QSOptimize_city`.`id` = `QSOptimize_person_visitation`.`city_id`)
WHERE `QSOptimize_person_visitation`.`person_id` IN (1, 2, 3, 4);

SELECT `QSOptimize_city`.`id`, `QSOptimize_city`.`name`, `QSOptimize_city`.`province_id` 
FROM `QSOptimize_city` 
INNER JOIN `QSOptimize_person_visitation` ON (`QSOptimize_city`.`id` = `QSOptimize_person_visitation`.`city_id`) 
WHERE(`QSOptimize_person_visitation`.`person_id` = 1  AND `QSOptimize_city`.`name` LIKE '%市%' );

SELECT `QSOptimize_city`.`id`, `QSOptimize_city`.`name`, `QSOptimize_city`.`province_id` 
FROM `QSOptimize_city` 
INNER JOIN `QSOptimize_person_visitation` ON (`QSOptimize_city`.`id` = `QSOptimize_person_visitation`.`city_id`) 
WHERE (`QSOptimize_person_visitation`.`person_id` = 2  AND `QSOptimize_city`.`name` LIKE '%市%' ); 

SELECT `QSOptimize_city`.`id`, `QSOptimize_city`.`name`, `QSOptimize_city`.`province_id` 
FROM `QSOptimize_city`
INNER JOIN `QSOptimize_person_visitation` ON (`QSOptimize_city`.`id` = `QSOptimize_person_visitation`.`city_id`) 
WHERE (`QSOptimize_person_visitation`.`person_id` = 3  AND `QSOptimize_city`.`name` LIKE '%市%' );

SELECT `QSOptimize_city`.`id`, `QSOptimize_city`.`name`, `QSOptimize_city`.`province_id` 
FROM `QSOptimize_city` 
INNER JOIN `QSOptimize_person_visitation` ON (`QSOptimize_city`.`id` = `QSOptimize_person_visitation`.`city_id`) 
WHERE (`QSOptimize_person_visitation`.`person_id` = 4  AND `QSOptimize_city`.`name` LIKE '%市%' );

详细分析一下这些请求事件。

众所周知,QuerySet是lazy的,要用的时候才会去访问数据库。运行到第二行Python代码时,for循环将plist看做iterator,这会触发数据库查询。最初的两次SQL查询就是prefetch_related导致的。

虽然已经查询结果中包含所有所需的city的信息,但因为在循环体中对Person.visitation进行了filter操作,这显然改变了数据库请求。因此这些操作会忽略掉之前缓存到的数据,重新进行SQL查询。

 

但是如果有这样的需求了应该怎么办呢?在Django >= 1.7,可以通过下一节的Prefetch对象来实现,如果你的环境是Django < 1.7,可以在Python中完成这部分操作。

plist = Person.objects.prefetch_related('visitation')
[[city for city in p.visitation.all() if u"市" in city.name] for p in plist]
Prefetch 对象

在Django >= 1.7,可以用Prefetch对象来控制prefetch_related函数的行为。

注:由于我没有安装1.7版本的Django环境,本节内容是参考Django文档写的,没有进行实际的测试。

 Prefetch对象的特征:

  1. 一个Prefetch对象只能指定一项prefetch操作。
  2. Prefetch对象对字段指定的方式和prefetch_related中的参数相同,都是通过双下划线连接的字段名完成的。
  3. 可以通过 queryset 参数手动指定prefetch使用的QuerySet。
  4. 可以通过 to_attr 参数指定prefetch到的属性名。
  5. Prefetch对象和字符串形式指定的lookups参数可以混用。

继续上面的例子,获取所有人访问过的城市中带有“武”字和“州”的城市:

wus = City.objects.filter(name__icontains = u"武")
zhous = City.objects.filter(name__icontains = u"州")
plist = Person.objects.prefetch_related(
    Prefetch('visitation', queryset = wus, to_attr = "wu_city"),
    Prefetch('visitation', queryset = zhous, to_attr = "zhou_city"),)
[p.wu_city for p in plist]
[p.zhou_city for p in plist]

注:这段代码没有在实际环境中测试过,若有不正确的地方请指正。

顺带一提,Prefetch对象和字符串参数可以混用。

None

可以通过传入一个None来清空之前的prefetch_related。就像这样:

>>> prefetch_cleared_qset = qset.prefetch_related(None)

小结

  1. prefetch_related主要针一对多和多对多关系进行优化。
  2. prefetch_related通过分别获取各个表的内容,然后用Python处理他们之间的关系来进行优化。
  3. 可以通过可变长参数指定需要select_related的字段名。指定方式和特征与select_related是相同的。
  4. 在Django >= 1.7可以通过Prefetch对象来实现复杂查询,但低版本的Django好像只能自己实现。
  5. 作为prefetch_related的参数,Prefetch对象和字符串可以混用。
  6. prefetch_related的链式调用会将对应的prefetch添加进去,而非替换,似乎没有基于不同版本上区别。
  7. 可以通过传入None来清空之前的prefetch_related。

4. 一些实例

选择哪个函数

如果我们想要获得所有家乡是湖北的人,最无脑的做法是先获得湖北省,再获得湖北的所有城市,最后获得故乡是这个城市的人。就像这样:

>>> hb = Province.objects.get(name__iexact=u"湖北省")
>>> people = []
>>> for city in hb.city_set.all():
...   people.extend(city.birth.all())
...

显然这不是一个明智的选择,因为这样做会导致1+(湖北省城市数)次SQL查询。反正是个反例,导致的查询和获得掉结果就不列出来了。

prefetch_related() 或许是一个好的解决方法,让我们来看看。

>>> hb = Province.objects.prefetch_related("city_set__birth").objects.get(name__iexact=u"湖北省")
>>> people = []
>>> for city in hb.city_set.all():
...   people.extend(city.birth.all())
...

因为是一个深度为2的prefetch,所以会导致3次SQL查询:

SELECT `QSOptimize_province`.`id`, `QSOptimize_province`.`name`
FROM `QSOptimize_province`
WHERE `QSOptimize_province`.`name` LIKE '湖北省' ;
 
SELECT `QSOptimize_city`.`id`, `QSOptimize_city`.`name`, `QSOptimize_city`.`province_id`
FROM `QSOptimize_city`
WHERE `QSOptimize_city`.`province_id` IN (1);
 
SELECT `QSOptimize_person`.`id`, `QSOptimize_person`.`firstname`, `QSOptimize_person`.`lastname`,
`QSOptimize_person`.`hometown_id`, `QSOptimize_person`.`living_id`
FROM `QSOptimize_person`
WHERE `QSOptimize_person`.`hometown_id` IN (1, 3);

嗯…看上去不错,但是3次查询么?倒过来查询可能会更简单?

>>> people = list(Person.objects.select_related("hometown__province").filter(hometown__province__name__iexact=u"湖北省"))
SELECT `QSOptimize_person`.`id`, `QSOptimize_person`.`firstname`, `QSOptimize_person`.`lastname`,
`QSOptimize_person`.`hometown_id`, `QSOptimize_person`.`living_id`, `QSOptimize_city`.`id`,
`QSOptimize_city`.`name`, `QSOptimize_city`.`province_id`, `QSOptimize_province`.`id`, `QSOptimize_province`.`name`
FROM `QSOptimize_person`
INNER JOIN `QSOptimize_city` ON (`QSOptimize_person`.`hometown_id` = `QSOptimize_city`.`id`)
INNER JOIN `QSOptimize_province` ON (`QSOptimize_city`.`province_id` = `QSOptimize_province`.`id`)
WHERE `QSOptimize_province`.`name` LIKE '湖北省';
+----+-----------+----------+-------------+-----------+----+--------+-------------+----+--------+
| id | firstname | lastname | hometown_id | living_id | id | name   | province_id | id | name   |
+----+-----------+----------+-------------+-----------+----+--------+-------------+----+--------+
|  1 | 张        | 三       |           3 |         1 |  3 | 十堰市 |           1 |  1 | 湖北省 |
|  2 | 李        | 四       |           1 |         3 |  1 | 武汉市 |           1 |  1 | 湖北省 |
|  3 | 王        | 麻子     |           3 |         2 |  3 | 十堰市 |           1 |  1 | 湖北省 |
+----+-----------+----------+-------------+-----------+----+--------+-------------+----+--------+
3 rows in set (0.00 sec)

完全没问题。不仅SQL查询的数量减少了,python程序上也精简了。

select_related()的效率要高于prefetch_related()。因此,最好在能用select_related()的地方尽量使用它,也就是说,对于ForeignKey字段,避免使用prefetch_related()。

联用

对于同一个QuerySet,你可以同时使用这两个函数。

在我们一直使用的例子上加一个model:Order (订单)

class Order(models.Model):
    customer   = models.ForeignKey(Person)
    orderinfo  = models.CharField(max_length=50)
    time       = models.DateTimeField(auto_now_add = True)
    def __unicode__(self):
        return self.orderinfo

如果我们拿到了一个订单的id 我们要知道这个订单的客户去过的省份。因为有ManyToManyField显然必须要用prefetch_related()。如果只用prefetch_related()会怎样呢?

>>> plist = Order.objects.prefetch_related('customer__visitation__province').get(id=1)
>>> for city in plist.customer.visitation.all():
...   print city.province.name
...

显然,关系到了4个表:Order、Person、City、Province,根据prefetch_related()的特性就得有4次SQL查询

SELECT `QSOptimize_order`.`id`, `QSOptimize_order`.`customer_id`, `QSOptimize_order`.`orderinfo`, `QSOptimize_order`.`time`
FROM `QSOptimize_order`
WHERE `QSOptimize_order`.`id` = 1 ;
 
SELECT `QSOptimize_person`.`id`, `QSOptimize_person`.`firstname`, `QSOptimize_person`.`lastname`, `QSOptimize_person`.`hometown_id`, `QSOptimize_person`.`living_id`
FROM `QSOptimize_person`
WHERE `QSOptimize_person`.`id` IN (1);
 
SELECT (`QSOptimize_person_visitation`.`person_id`) AS `_prefetch_related_val`, `QSOptimize_city`.`id`,
`QSOptimize_city`.`name`, `QSOptimize_city`.`province_id`
FROM `QSOptimize_city`
INNER JOIN `QSOptimize_person_visitation` ON (`QSOptimize_city`.`id` = `QSOptimize_person_visitation`.`city_id`)
WHERE `QSOptimize_person_visitation`.`person_id` IN (1);
 
SELECT `QSOptimize_province`.`id`, `QSOptimize_province`.`name`
FROM `QSOptimize_province`
WHERE `QSOptimize_province`.`id` IN (1, 2);
+----+-------------+---------------+---------------------+
| id | customer_id | orderinfo     | time                |
+----+-------------+---------------+---------------------+
|  1 |           1 | Info of Order | 2014-08-10 17:05:48 |
+----+-------------+---------------+---------------------+
1 row in set (0.00 sec)
 
+----+-----------+----------+-------------+-----------+
| id | firstname | lastname | hometown_id | living_id |
+----+-----------+----------+-------------+-----------+
|  1 | 张        | 三       |           3 |         1 |
+----+-----------+----------+-------------+-----------+
1 row in set (0.00 sec)
 
+-----------------------+----+--------+-------------+
| _prefetch_related_val | id | name   | province_id |
+-----------------------+----+--------+-------------+
|                     1 |  1 | 武汉市 |           1 |
|                     1 |  2 | 广州市 |           2 |
|                     1 |  3 | 十堰市 |           1 |
+-----------------------+----+--------+-------------+
3 rows in set (0.00 sec)
 
+----+--------+
| id | name   |
+----+--------+
|  1 | 湖北省 |
|  2 | 广东省 |
+----+--------+
2 rows in set (0.00 sec)

更好的办法是先调用一次select_related()再调用prefetch_related(),最后再select_related()后面的表

>>> plist = Order.objects.select_related('customer').prefetch_related('customer__visitation__province').get(id=1)
>>> for city in plist.customer.visitation.all():
...   print city.province.name
...

这样只会有3次SQL查询,Django会先做select_related,之后prefetch_related的时候会利用之前缓存的数据,从而避免了1次额外的SQL查询:

SELECT `QSOptimize_order`.`id`, `QSOptimize_order`.`customer_id`, `QSOptimize_order`.`orderinfo`, 
`QSOptimize_order`.`time`, `QSOptimize_person`.`id`, `QSOptimize_person`.`firstname`, 
`QSOptimize_person`.`lastname`, `QSOptimize_person`.`hometown_id`, `QSOptimize_person`.`living_id` 
FROM `QSOptimize_order` 
INNER JOIN `QSOptimize_person` ON (`QSOptimize_order`.`customer_id` = `QSOptimize_person`.`id`) 
WHERE `QSOptimize_order`.`id` = 1 ;
 
SELECT (`QSOptimize_person_visitation`.`person_id`) AS `_prefetch_related_val`, `QSOptimize_city`.`id`, 
`QSOptimize_city`.`name`, `QSOptimize_city`.`province_id` 
FROM `QSOptimize_city` 
INNER JOIN `QSOptimize_person_visitation` ON (`QSOptimize_city`.`id` = `QSOptimize_person_visitation`.`city_id`) 
WHERE `QSOptimize_person_visitation`.`person_id` IN (1);
 
SELECT `QSOptimize_province`.`id`, `QSOptimize_province`.`name` 
FROM `QSOptimize_province` 
WHERE `QSOptimize_province`.`id` IN (1, 2);
+----+-------------+---------------+---------------------+----+-----------+----------+-------------+-----------+
| id | customer_id | orderinfo     | time                | id | firstname | lastname | hometown_id | living_id |
+----+-------------+---------------+---------------------+----+-----------+----------+-------------+-----------+
|  1 |           1 | Info of Order | 2014-08-10 17:05:48 |  1 | 张        | 三       |           3 |         1 |
+----+-------------+---------------+---------------------+----+-----------+----------+-------------+-----------+
1 row in set (0.00 sec)
 
+-----------------------+----+--------+-------------+
| _prefetch_related_val | id | name   | province_id |
+-----------------------+----+--------+-------------+
|                     1 |  1 | 武汉市 |           1 |
|                     1 |  2 | 广州市 |           2 |
|                     1 |  3 | 十堰市 |           1 |
+-----------------------+----+--------+-------------+
3 rows in set (0.00 sec)
 
+----+--------+
| id | name   |
+----+--------+
|  1 | 湖北省 |
|  2 | 广东省 |
+----+--------+
2 rows in set (0.00 sec)

小结

  1. 因为select_related()总是在单次SQL查询中解决问题,而prefetch_related()会对每个相关表进行SQL查询,因此select_related()的效率通常比后者高。
  2. 鉴于第一条,尽可能的用select_related()解决问题。只有在select_related()不能解决问题的时候再去想prefetch_related()。
  3. 你可以在一个QuerySet中同时使用select_related()和prefetch_related(),从而减少SQL查询的次数。
  4. 只有prefetch_related()之前的select_related()是有效的,之后的将会被无视掉。

转自

查看原文

赞 1 收藏 0 评论 0

rayzz 关注了用户 · 2018-08-25

布客飞龙 @wizardforcel

欢迎来星球做客:t.zsxq.com/Jq3vZZB

请关注我们的公众号“ApacheCN”,回复“教程/路线/比赛/报告/技术书/轻小说/漫画/新知”来获取更多资源。

关注 784

rayzz 关注了用户 · 2018-08-25

Andy @andy_5b2f636406f35

关注 2

rayzz 关注了用户 · 2018-08-25

sjtuzyl @sjtuzyl

关注 3

认证与成就

  • 获得 42 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-07-11
个人主页被 620 人浏览