first commit
252
04.Flask/01.URL与视图.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# URL与视图
|
||||
|
||||
## 1. Flask简介
|
||||
|
||||
`flask`是一款非常流行的`Python Web`框架,出生于2010年,作者是`Armin Ronacher`,本来这个项目只是作者在愚人节的一个玩笑,后来由于非常受欢迎,进而成为一个正式的项目。
|
||||
|
||||
`flask`自2010年发布第一个版本以来,大受欢迎,深得开发者的喜爱,目前在`Github`上的Star数已经超过`55.5k`了,有超`Django`之趋势。`flask`能如此流行的原因,可以分为以下几点:
|
||||
|
||||
- 微框架、简洁、只做他需要做的,给开发者提供了很大的扩展性。
|
||||
- Flask和相应的插件写得很好,用起来很爽。
|
||||
- 开发效率非常高,比如使用`SQLAlchemy`的`ORM`操作数据库可以节省开发者大量书写`sql`的时间。
|
||||
|
||||
`Flask`的灵活度非常之高,他不会帮你做太多的决策,一些你都可以按照自己的意愿进行更改。比如:
|
||||
|
||||
- 使用`Flask`开发数据库的时候,具体是使用`SQLAlchemy`还是`MongoEngine`,选择权完全掌握在你自己的手中。区别于`Django`,`Django`内置了非常完善和丰富的功能,并且如果你想替换成你自己想要的,要么不支持,要么非常麻烦。
|
||||
- 把默认的`Jinija2`模板引擎替换成其他模板引擎都是非常容易的。
|
||||
|
||||
### 1.1 安装Flask
|
||||
|
||||
通过`pip install flask`即可安装。
|
||||
|
||||
### 1.2 第一个flask程序
|
||||
|
||||
用`pycharm`新建一个`flask`项目,新建项目的截图如下:
|
||||
|
||||

|
||||
|
||||
点击`create`后创建一个新项目,然后在`helloworld.py`文件中书写代码:
|
||||
|
||||
```python
|
||||
#coding: utf8
|
||||
|
||||
# 从flask框架中导入Flask类
|
||||
from flask import Flask
|
||||
|
||||
# 传入__name__初始化一个Flask实例
|
||||
app = Flask(__name__)
|
||||
|
||||
# app.route装饰器映射URL和执行的函数。这个设置将根URL映射到了hello_world函数上
|
||||
@app.route('/')
|
||||
def hello_world():
|
||||
return 'Hello World!'
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 运行本项目,host=0.0.0.0可以让其他电脑也能访问到该网站,port指定访问的端口。默认的host是127.0.0.1,port为5000
|
||||
app.run(host='0.0.0.0',port=9000)
|
||||
```
|
||||
|
||||
然后点击运行,在浏览器中输入`http://127.0.0.1:9000`就能看到`hello world`了。需要说明一点的是,`app.run`这种方式只适合于开发,如果在生产环境中,应该使用`Gunicorn`或者`uWSGI`来启动。如果是在终端运行的,可以按`ctrl+c`来让服务停止。
|
||||
|
||||
## 2. 项目配置
|
||||
|
||||
### 2.1 设置为DEBUG模式
|
||||
|
||||
默认情况下`flask`不会开启`DEBUG`模式,开启`DEBUG`模式后,flask会在每次保存代码的时候自动的重新载入代码,并且如果代码有错误,会在终端进行提示。
|
||||
|
||||

|
||||
|
||||
如果一切正常,会在终端打印以下信息:
|
||||
|
||||
```
|
||||
* Restarting with stat
|
||||
* Debugger is active!
|
||||
* Debugger pin code: 294-745-044
|
||||
* Running on http://0.0.0.0:9000/ (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
需要注意的是,只能在开发环境下开启`DEBUG`模式,因为`DEBUG`模式会带来非常大的安全隐患。
|
||||
|
||||
### 2.2 配置文件
|
||||
|
||||
`Flask`项目的配置,都是通过`app.config`对象来进行配置的。比如要配置一个项目的`SECRET_KEY`,那么可以使用`app.config['SECRET_KEY'] = "xxx"`来进行设置,在`Flask`项目中,有四种方式进行项目的配置:
|
||||
|
||||
1. 直接硬编码:
|
||||
|
||||
```python
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = "xxx"
|
||||
```
|
||||
|
||||
2. 因为`app.config`是`flask.config.Config`的实例,而`Config`类是继承自`dict`,因此可以通过`update`方法:
|
||||
|
||||
```python
|
||||
app.config.update(
|
||||
DEBUG=True,
|
||||
SECRET_KEY='...'
|
||||
)
|
||||
```
|
||||
|
||||
3. 如果你的配置项特别多,你可以把所有的配置项都放在一个模块中,然后通过加载模块的方式进行配置,假设有一个`settings.py`模块,专门用来存储配置项的,此时你可以通过`app.config.from_object()`方法进行加载,并且该方法既可以接收模块的的字符串名称,也可以模块对象:
|
||||
|
||||
```python
|
||||
# 1. 通过模块字符串
|
||||
app.config.from_object('settings')
|
||||
# 2. 通过模块对象
|
||||
import settings
|
||||
app.config.from_object(settings)
|
||||
```
|
||||
|
||||
4. 也可以通过另外一个方法加载,该方法就是`app.config.from_pyfile()`,该方法传入一个文件名,通常是以`.py`结尾的文件,但也不限于只使用`.py`后缀的文件:
|
||||
|
||||
```python
|
||||
app.config.from_pyfile('settings.py',silent=True)
|
||||
# silent=True表示如果配置文件不存在的时候不抛出异常,默认是为False,会抛出异常。
|
||||
```
|
||||
|
||||
## 3. URL与视图
|
||||
|
||||
### 3.1 URL与函数的映射
|
||||
|
||||
从之前的`helloworld.py`文件中,我们已经看到,一个`URL`要与执行函数进行映射,使用的是`@app.route`装饰器。`@app.route`装饰器中,可以指定`URL`的规则来进行更加详细的映射,比如现在要映射一个文章详情的`URL`,文章详情的`URL`是`/article/id/`,id有可能为1、2、3…,那么可以通过以下方式:
|
||||
|
||||
```python
|
||||
@app.route('/article/<id>/')
|
||||
def article(id):
|
||||
return '%s article detail' % id
|
||||
```
|
||||
|
||||
其中`<id>`,尖括号是固定写法,语法为`<variable>`,`variable`默认的数据类型是字符串。如果需要指定类型,则要写成`<converter:variable>`,其中`converter`就是类型名称,可以有以下几种:
|
||||
|
||||
- string: 默认的数据类型,接受没有任何斜杠`/`的字符串。
|
||||
- int: 整形
|
||||
- float: 浮点型。
|
||||
- path: 和`string`类似,但是可以传递斜杠`/`。
|
||||
- uuid: `uuid`类型的字符串。
|
||||
- any:可以指定多种路径,这个通过一个例子来进行说明:
|
||||
|
||||
```python
|
||||
@app.route('/<any(article,blog):url_path>/')
|
||||
def item(url_path):
|
||||
return url_path
|
||||
```
|
||||
|
||||
以上例子中,`item`这个函数可以接受两个`URL`,一个是`/article/`,另一个是`/blog/`。并且,一定要传`url_path`参数,当然这个`url_path`的名称可以随便。
|
||||
|
||||
如果不想定制子路径来传递参数,也可以通过传统的`?=`的形式来传递参数,例如:`/article?id=xxx`,这种情况下,可以通过`request.args.get('id')`来获取`id`的值。如果是`post`方法,则可以通过`request.form.get('id')`来进行获取。
|
||||
|
||||
### 3.2 构造URL(url_for)
|
||||
|
||||
一般我们通过一个`URL`就可以执行到某一个函数。如果反过来,我们知道一个函数,怎么去获得这个`URL`呢?`url_for`函数就可以帮我们实现这个功能。`url_for()`函数接收两个及以上的参数,他接收**函数名**作为第一个参数,接收对应**URL规则的命名参数**,如果还出现其他的参数,则会添加到`URL`的后面作为**查询参数**。
|
||||
|
||||
通过构建`URL`的方式而选择直接在代码中拼`URL`的原因有两点:
|
||||
|
||||
1. 将来如果修改了`URL`,但没有修改该`URL`对应的函数名,就不用到处去替换`URL`了。
|
||||
2. `url_for()`函数会转义一些特殊字符和`unicode`字符串,这些事情`url_for`会自动的帮我们搞定。
|
||||
|
||||
下面用一个例子来进行解释:
|
||||
|
||||
```python
|
||||
from flask import Flask,url_for
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/article/<id>/')
|
||||
def article(id):
|
||||
return '%s article detail' % id
|
||||
|
||||
@app.route('/')
|
||||
def index(request):
|
||||
print(url_for("article",id=1))
|
||||
return "首页"
|
||||
```
|
||||
|
||||
在访问index的时候,会打印出`/article/1/`。
|
||||
|
||||
### 3.3 指定URL末尾的斜杠
|
||||
|
||||
有些`URL`的末尾是有斜杠的,有些`URL`末尾是没有斜杠的。这其实是两个不同的`URL`。
|
||||
|
||||
举个例子:
|
||||
|
||||
```python
|
||||
@app.route('/article/')
|
||||
def articles():
|
||||
return '文章列表页'
|
||||
```
|
||||
|
||||
上述例子中,当访问一个结尾不带斜线的`URL`:`/article`,会被重定向到带斜线的`URL`:`/article/`上去。但是当我们在定义`article`的`url`的时候,如果在末尾没有加上斜杠,但是在访问的时候又加上了斜杠,这时候就会抛出一个`404`错误页面了:
|
||||
|
||||
```python
|
||||
@app.route("/article")
|
||||
def articles(request):
|
||||
return "文章列表页面"
|
||||
```
|
||||
|
||||
以上没有在末尾加斜杠,因此在访问`/article/`的时候,就会抛出一个`404`错误。
|
||||
|
||||
### 3.4 指定HTTP方法
|
||||
|
||||
在`@app.route()`中可以传入一个关键字参数`methods`来指定本方法支持的`HTTP`方法,默认情况下,只能使用`GET`请求,看以下例子:
|
||||
|
||||
```python
|
||||
@app.route('/login/',methods=['GET','POST'])
|
||||
def login():
|
||||
return 'login'
|
||||
```
|
||||
|
||||
以上装饰器将让`login`的`URL`既能支持`GET`又能支持`POST`。
|
||||
|
||||
### 3.5 页面跳转和重定向
|
||||
|
||||
重定向分为永久性重定向和暂时性重定向,在页面上体现的操作就是浏览器会从一个页面自动跳转到另外一个页面。比如用户访问了一个需要权限的页面,但是该用户当前并没有登录,因此我们应该给他重定向到登录页面。
|
||||
|
||||
- 永久性重定向:`http`的状态码是`301`,多用于旧网址被废弃了要转到一个新的网址确保用户的访问,最经典的就是京东网站,你输入`www.jingdong.com`的时候,会被重定向到`www.jd.com`,因为`jingdong.com`这个网址已经被废弃了,被改成`jd.com`,所以这种情况下应该用永久重定向。
|
||||
- 暂时性重定向:`http`的状态码是`302`,表示页面的暂时性跳转。比如访问一个需要权限的网址,如果当前用户没有登录,应该重定向到登录页面,这种情况下,应该用暂时性重定向。
|
||||
|
||||
在`flask`中,重定向是通过`flask.redirect(location,code=302)`这个函数来实现的,`location`表示需要重定向到的`URL`,应该配合之前讲的`url_for()`函数来使用,`code`表示采用哪个重定向,默认是`302`也即`暂时性重定向`,可以修改成`301`来实现永久性重定向。
|
||||
|
||||
以下来看一个例子,关于在`flask`中怎么使用重定向:
|
||||
|
||||
```python
|
||||
from flask import Flask,url_for,redirect
|
||||
|
||||
app = Flask(__name__)
|
||||
app.debug = True
|
||||
|
||||
@app.route('/login/',methods=['GET','POST'])
|
||||
def login():
|
||||
return 'login page'
|
||||
|
||||
@app.route('/profile/',methods=['GET','POST'])
|
||||
def profile():
|
||||
name = request.args.get('name')
|
||||
|
||||
if not name:
|
||||
# 如果没有name,说明没有登录,重定向到登录页面
|
||||
return redirect(url_for('login'))
|
||||
else:
|
||||
return name
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
BIN
04.Flask/01.URL与视图/2UZz2JSDySff5o8CPeqPRN.png
Normal file
After Width: | Height: | Size: 142 KiB |
BIN
04.Flask/01.URL与视图/87L8AaNBNKGcAAn7oomKYT.png
Normal file
After Width: | Height: | Size: 177 KiB |
493
04.Flask/02.Jinja2模板.md
Normal file
@@ -0,0 +1,493 @@
|
||||
# Jinja2模板
|
||||
|
||||
## 1. 模板简介
|
||||
|
||||
模板是一个`web`开发必备的模块。因为我们在渲染一个网页的时候,并不是只渲染一个纯文本字符串,而是需要渲染一个有富文本标签的页面。这时候我们就需要使用模板了。在`Flask`中,配套的模板是`Jinja2`,`Jinja2`的作者也是`Flask`的作者。这个模板非常的强大,并且执行效率高。以下对`Jinja2`做一个简单介绍!
|
||||
|
||||
### 1.1 Flask渲染`Jinja`模板:
|
||||
|
||||
要渲染一个模板,通过`render_template`方法即可,以下将用一个简单的例子进行讲解:
|
||||
|
||||
```python
|
||||
from flask import Flask,render_template
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/about/')
|
||||
def about():
|
||||
return render_template('about.html')
|
||||
```
|
||||
|
||||
当访问`/about/`的时候,`about()`函数会在当前目录下的`templates`文件夹下寻找`about.html`模板文件。如果想更改模板文件地址,应该在创建`app`的时候,给`Flask`传递一个关键字参数`template_folder`,指定具体的路径,再看以下例子:
|
||||
|
||||
```python
|
||||
from flask import Flask,render_template
|
||||
app = Flask(__name__,template_folder=r'C:\templates')
|
||||
|
||||
@app.route('/about/')
|
||||
def about():
|
||||
return render_template('about.html')
|
||||
```
|
||||
|
||||
以上例子将会在C盘的`templates`文件夹中寻找模板文件。还有最后一点是,如果模板文件中有参数需要传递,应该怎么传呢,我们再来看一个例子:
|
||||
|
||||
```python
|
||||
from flask import Flask,render_template
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/about/')
|
||||
def about():
|
||||
# return render_template('about.html',user='zhiliao')
|
||||
return render_template('about.html',**{'user':'zhiliao'})
|
||||
```
|
||||
|
||||
以上例子介绍了两种传递参数的方式,因为`render_template`需要传递的是一个关键字参数,所以第一种方式是顺其自然的。但是当你的模板中要传递的参数过多的时候,把所有参数放在一个函数中显然不是一个好的选择,因此我们使用字典进行包装,并且加两个`*`号,来转换成关键字参数。
|
||||
|
||||
## 2. Jinja2模版概述
|
||||
|
||||
### 2.1 概要:
|
||||
|
||||
先看一个简单例子:
|
||||
|
||||
```html
|
||||
1. <html lang="en">
|
||||
2. <head>
|
||||
3. <title>My Webpage</title>
|
||||
4. </head>
|
||||
5. <body>
|
||||
6. <ul id="navigation">
|
||||
7. {% for item in navigation %}
|
||||
8. <li><a href="{{ item.href }}">{{ item.caption }}</a></li>
|
||||
9. {% endfor %}
|
||||
10. </ul>
|
||||
11.
|
||||
12. {{ a_variable }}
|
||||
13. {{ user.name }}
|
||||
14. {{ user['name'] }}
|
||||
15.
|
||||
16. {# a comment #}
|
||||
17. </body>
|
||||
18.</html>
|
||||
```
|
||||
|
||||
以上示例有需要进行解释:
|
||||
|
||||
- 第12~14行的`{{ ... }}`:用来装载一个变量,模板渲染的时候,会把这个变量代表的值替换掉。并且可以间接访问一个变量的属性或者一个字典的`key`。关于点`.`号访问和`[]`中括号访问,没有任何区别,都可以访问属性和字典的值。
|
||||
- 第7~9行的`{% ... %}`:用来装载一个控制语句,以上装载的是`for`循环,以后只要是要用到控制语句的,就用`{% ... %}`。
|
||||
- 第14行的`{# ... #}`:用来装载一个注释,模板渲染的时候会忽视这中间的值。
|
||||
|
||||
### 2.2 属性访问规则:
|
||||
|
||||
1. 比如在模板中有一个变量这样使用:`foo.bar`,那么在`Jinja2`中是这样进行访问的:
|
||||
- 先去查找`foo`的`bar`这个属性,也即通过`getattr(foo,'bar')`。
|
||||
- 如果没有,就去通过`foo.__getitem__('bar')`的方式进行查找。
|
||||
- 如果以上两种方式都没有找到,返回一个`undefined`。
|
||||
2. 在模板中有一个变量这样使用:`foo['bar']`,那么在`Jinja2`中是这样进行访问:
|
||||
- 通过`foo.__getitem__('bar')`的方式进行查找。
|
||||
- 如果没有,就通过`getattr(foo,'bar')`的方式进行查找。
|
||||
- 如果以上没有找到,则返回一个`undefined`。
|
||||
|
||||
## 3. Jinja2模版过滤器
|
||||
|
||||
过滤器是通过管道符号(`|`)进行使用的,例如:`{{ name|length }}`,将返回name的长度。过滤器相当于是一个函数,把当前的变量传入到过滤器中,然后过滤器根据自己的功能,再返回相应的值,之后再将结果渲染到页面中。`Jinja2`中内置了许多过滤器,在[这里](https://jinja.palletsprojects.com/en/3.0.x/templates/#list-of-builtin-filters)可以看到所有的过滤器,现对一些常用的过滤器进行讲解:
|
||||
|
||||
1. `abs(value)`:返回一个数值的绝对值。 例如:`-1|abs`。
|
||||
|
||||
2. `default(value,default_value,boolean=false)`:如果当前变量没有值,则会使用参数中的值来代替。`name|default('xiaotuo')`——如果name不存在,则会使用`xiaotuo`来替代。`boolean=False`默认是在只有这个变量为`undefined`的时候才会使用`default`中的值,如果想使用`python`的形式判断是否为`false`,则可以传递`boolean=true`。也可以使用`or`来替换。
|
||||
|
||||
3. `escape(value)或e`:转义字符,会将`<`、`>`等符号转义成HTML中的符号。例如:`content|escape`或`content|e`。
|
||||
|
||||
4. `first(value)`:返回一个序列的第一个元素。`names|first`。
|
||||
|
||||
5. `format(value,*arags,**kwargs)`:格式化字符串。例如以下代码:
|
||||
|
||||
```jinja2
|
||||
{{ "%s" - "%s"|format('Hello?',"Foo!") }}
|
||||
```
|
||||
|
||||
将输出:Helloo? - Foo!
|
||||
|
||||
6. `last(value)`:返回一个序列的最后一个元素。示例:`names|last`。
|
||||
|
||||
7. `length(value)`:返回一个序列或者字典的长度。示例:`names|length`。
|
||||
|
||||
8. `join(value,d=u'')`:将一个序列用`d`这个参数的值拼接成字符串。
|
||||
|
||||
9. `safe(value)`:如果开启了全局转义,那么`safe`过滤器会将变量关掉转义。示例:`content_html|safe`。
|
||||
|
||||
10. `int(value)`:将值转换为`int`类型。
|
||||
|
||||
11. `float(value)`:将值转换为`float`类型。
|
||||
|
||||
12. `lower(value)`:将字符串转换为小写。
|
||||
|
||||
13. `upper(value)`:将字符串转换为小写。
|
||||
|
||||
14. `replace(value,old,new)`: 替换将`old`替换为`new`的字符串。
|
||||
|
||||
15. `truncate(value,length=255,killwords=False)`:截取`length`长度的字符串。
|
||||
|
||||
16. `striptags(value)`:删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格。
|
||||
|
||||
17. `trim`:截取字符串前面和后面的空白字符。
|
||||
|
||||
18. `string(value)`:将变量转换成字符串。
|
||||
|
||||
19. `wordcount(s)`:计算一个长字符串中单词的个数。
|
||||
|
||||
## 4. 控制语句
|
||||
|
||||
所有的控制语句都是放在`{% ... %}`中,并且有一个语句`{% endxxx %}`来进行结束,`Jinja`中常用的控制语句有`if/for..in..`,现对他们进行讲解:
|
||||
|
||||
1. `if`:if语句和`python`中的类似,可以使用`>,<,<=,>=,==,!=`来进行判断,也可以通过`and,or,not,()`来进行逻辑合并操作,以下看例子:
|
||||
|
||||
```jinja2
|
||||
{% if kenny.sick %}
|
||||
Kenny is sick.
|
||||
{% elif kenny.dead %}
|
||||
You killed Kenny! You bastard!!!
|
||||
{% else %}
|
||||
Kenny looks okay --- so far
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
2. `for...in...`:`for`循环可以遍历任何一个序列包括列表、字典、元组。并且可以进行反向遍历,以下将用几个例子进行解释:
|
||||
|
||||
- 普通的遍历:
|
||||
|
||||
```jinja2
|
||||
<ul>
|
||||
{% for user in users %}
|
||||
<li>{{ user.username|e }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
```
|
||||
|
||||
- 遍历字典:
|
||||
|
||||
```jinja2
|
||||
<dl>
|
||||
{% for key, value in my_dict.iteritems() %}
|
||||
<dt>{{ key|e }}</dt>
|
||||
<dd>{{ value|e }}</dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
```
|
||||
|
||||
- 如果序列中没有值的时候,进入`else`:
|
||||
|
||||
```jinja2
|
||||
<ul>
|
||||
{% for user in users %}
|
||||
<li>{{ user.username|e }}</li>
|
||||
{% else %}
|
||||
<li><em>no users found</em></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
```
|
||||
|
||||
并且`Jinja`中的`for`循环还包含以下变量,可以用来获取当前的遍历状态:
|
||||
|
||||
| 变量 | 描述 |
|
||||
| :---------- | :---------------------------------- |
|
||||
| loop.index | 当前迭代的索引(从1开始) |
|
||||
| loop.index0 | 当前迭代的索引(从0开始) |
|
||||
| loop.first | 是否是第一次迭代,返回True或False |
|
||||
| loop.last | 是否是最后一次迭代,返回True或False |
|
||||
| loop.length | 序列的长度 |
|
||||
|
||||
另外,**不可以**使用`continue`和`break`表达式来控制循环的执行。
|
||||
|
||||
## 5. 测试器
|
||||
|
||||
测试器主要用来判断一个值是否满足某种类型,并且这种类型一般通过普通的`if`判断是有很大的挑战的。语法是:`if...is...`,先来简单的看个例子:
|
||||
|
||||
```jinja2
|
||||
{% if variable is escaped%}
|
||||
value of variable: {{ escaped }}
|
||||
{% else %}
|
||||
variable is not escaped
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
以上判断`variable`这个变量是否已经被转义了,`Jinja`中内置了许多的测试器,看以下列表:
|
||||
|
||||
| 测试器 | 说明 |
|
||||
| :----------------- | :----------------- |
|
||||
| `callable(object)` | 是否可调用 |
|
||||
| `defined(object)` | 是否已经被定义了。 |
|
||||
| `escaped(object)` | 是否已经被转义了。 |
|
||||
| `upper(object)` | 是否全是大写。 |
|
||||
| `lower(object)` | 是否全是小写。 |
|
||||
| `string(object)` | 是否是一个字符串。 |
|
||||
| `sequence(object)` | 是否是一个序列。 |
|
||||
| `number(object)` | 是否是一个数字。 |
|
||||
| `odd(object)` | 是否是奇数。 |
|
||||
| `even(object)` | 是否是偶数。 |
|
||||
|
||||
## 6. 宏和import语句
|
||||
|
||||
### 6.1 宏:
|
||||
|
||||
模板中的宏跟python中的函数类似,可以传递参数,但是不能有返回值,可以将一些经常用到的代码片段放到宏中,然后把一些不固定的值抽取出来当成一个变量,以下将用一个例子来进行解释:
|
||||
|
||||
```jinja2
|
||||
{% macro input(name, value='', type='text') %}
|
||||
<input type="{{ type }}" name="{{ name }}" value="{{ value|e }}">
|
||||
{% endmacro %}
|
||||
```
|
||||
|
||||
以上例子可以抽取出了一个input标签,指定了一些默认参数。那么我们以后创建`input`标签的时候,可以通过他快速的创建:
|
||||
|
||||
```jinja2
|
||||
<p>{{ input('username') }}</p>
|
||||
<p>{{ input('password', type='password') }}</p>
|
||||
```
|
||||
|
||||
### 6.2 import语句:
|
||||
|
||||
在真实的开发中,会将一些常用的宏单独放在一个文件中,在需要使用的时候,再从这个文件中进行导入。`import`语句的用法跟`python`中的`import`类似,可以直接`import...as...`,也可以`from...import...`或者`from...import...as...`,假设现在有一个文件,叫做`forms.html`,里面有两个宏分别为`input`和`textarea`,如下:
|
||||
|
||||
```jinja2
|
||||
{% macro input(name, value='', type='text') %}
|
||||
<input type="{{ type }}" value="{{ value|e }}" name="{{ name }}">
|
||||
{% endmacro %}
|
||||
|
||||
{% macro textarea(name, value='', rows=10, cols=40) %}
|
||||
<textarea name="{{ name }}" rows="{{ rows }}" cols="{{ cols
|
||||
}}">{{ value|e }}</textarea>
|
||||
{% endmacro %}
|
||||
```
|
||||
|
||||
### 6.3 导入宏的例子:
|
||||
|
||||
1. `import...as...`形式:
|
||||
|
||||
```jinja2
|
||||
{% import 'forms.html' as forms %}
|
||||
<dl>
|
||||
<dt>Username</dt>
|
||||
<dd>{{ forms.input('username') }}</dd>
|
||||
<dt>Password</dt>
|
||||
<dd>{{ forms.input('password', type='password') }}</dd>
|
||||
</dl>
|
||||
<p>{{ forms.textarea('comment') }}</p>
|
||||
```
|
||||
|
||||
2. `from...import...as.../from...import...`形式:
|
||||
|
||||
```jinja2
|
||||
{% from 'forms.html' import input as input_field, textarea %}
|
||||
<dl>
|
||||
<dt>Username</dt>
|
||||
<dd>{{ input_field('username') }}</dd>
|
||||
<dt>Password</dt>
|
||||
<dd>{{ input_field('password', type='password') }}</dd>
|
||||
</dl>
|
||||
<p>{{ textarea('comment') }}</p>
|
||||
```
|
||||
|
||||
另外需要注意的是,导入模板并不会把当前上下文中的变量添加到被导入的模板中,如果你想要导入一个需要访问当前上下文变量的宏,有两种可能的方法:
|
||||
|
||||
- 显式地传入请求或请求对象的属性作为宏的参数。
|
||||
|
||||
- 与上下文一起(with context)导入宏。
|
||||
|
||||
与上下文中一起(with context)导入的方式如下:
|
||||
|
||||
```jinja2
|
||||
{% from '_helpers.html' import my_macro with context %}
|
||||
```
|
||||
|
||||
## 7. include和set语句
|
||||
|
||||
### 7.1 include语句:
|
||||
|
||||
`include`语句可以把一个模板引入到另外一个模板中,类似于把一个模板的代码copy到另外一个模板的指定位置,看以下例子:
|
||||
|
||||
```jinja2
|
||||
{% include 'header.html' %}
|
||||
主体内容
|
||||
{% include 'footer.html' %}
|
||||
```
|
||||
|
||||
### 7.2 赋值(set)语句:
|
||||
|
||||
有时候我们想在在模板中添加变量,这时候赋值语句(set)就派上用场了,先看以下例子:
|
||||
|
||||
```jinja2
|
||||
{% set name='zhiliao' %}
|
||||
```
|
||||
|
||||
那么以后就可以使用`name`来代替`zhiliao`这个值了,同时,也可以给他赋值为列表和元组:
|
||||
|
||||
```jinja2
|
||||
{% set navigation = [('index.html', 'Index'), ('about.html', 'About')] %}
|
||||
```
|
||||
|
||||
赋值语句创建的变量在其之后都是有效的,如果不想让一个变量污染全局环境,可以使用`with`语句来创建一个内部的作用域,将`set`语句放在其中,这样创建的变量只在`with`代码块中才有效,看以下示例:
|
||||
|
||||
```jinja2
|
||||
{% with %}
|
||||
{% set foo = 42 %}
|
||||
{{ foo }} foo is 42 here
|
||||
{% endwith %}
|
||||
```
|
||||
|
||||
也可以在`with`的后面直接添加变量,比如以上的写法可以修改成这样:
|
||||
|
||||
```jinja2
|
||||
{% with foo = 42 %}
|
||||
{{ foo }}
|
||||
{% endwith %}
|
||||
```
|
||||
|
||||
这两种方式都是等价的,一旦超出`with`代码块,就不能再使用`foo`这个变量了。
|
||||
|
||||
## 8. 模版继承
|
||||
|
||||
`Flask`中的模板可以继承,通过继承可以把模板中许多重复出现的元素抽取出来,放在父模板中,并且父模板通过定义`block`给子模板开一个口,子模板根据需要,再实现这个`block`,假设现在有一个`base.html`这个父模板,代码如下:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="stylesheet" href="base.css" />
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div id="body">{% block body %}{% endblock %}</div>
|
||||
<div id="footer">
|
||||
{% block footer %}
|
||||
© Copyright 2008 by <a href="http://domain.invalid/">you</a>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
以上父模板中,抽取了所有模板都需要用到的元素`html`、`body`等,并且对于一些所有模板都要用到的样式文件`style.css`也进行了抽取,同时对于一些子模板需要重写的地方,比如`title`、`head`、`body`都定义成了`block`,然后子模板可以根据自己的需要,再具体的实现。以下再来看子模板的代码:
|
||||
|
||||
```jinja2
|
||||
{% extends "base.html" %}
|
||||
{% block title %}首页{% endblock %}
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<style type="text/css">
|
||||
.detail{
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h1>这里是首页</h1>
|
||||
<p class="detail">
|
||||
首页的内容
|
||||
</p>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
首先第一行就定义了子模板继承的父模板,并且可以看到子模板实现了`title`这个`block`,并填充了自己的内容,再看`head`这个`block`,里面调用了`super()`这个函数,这个函数的目的是执行父模板中的代码,把父模板中的内容添加到子模板中,如果没有这一句,则父模板中处在`head`这个`block`中的代码将会被子模板中的代码给覆盖掉。
|
||||
|
||||
另外,模板中不能出现重名的`block`,如果一个地方需要用到另外一个`block`中的内容,可以使用`self.blockname`的方式进行引用,比如以下示例:
|
||||
|
||||
```html
|
||||
<title>
|
||||
{% block title %}
|
||||
这是标题
|
||||
{% endblock %}
|
||||
</title>
|
||||
<h1>{{ self.title() }}</h1>
|
||||
```
|
||||
|
||||
以上示例中`h1`标签重用了`title`这个`block`中的内容,子模板实现了`title`这个`block`,`h1`标签也能拥有这个值。
|
||||
|
||||
另外,在子模板中,所有的文本标签和代码都要添加到从父模板中继承的`block`中。否则,这些文本和标签将不会被渲染。
|
||||
|
||||
## 9. 转义
|
||||
|
||||
转义的概念是,在模板渲染字符串的时候,字符串有可能包括一些非常危险的字符比如`<`、`>`等,这些字符会破坏掉原来`HTML`标签的结构,更严重的可能会发生`XSS`跨域脚本攻击,因此如果碰到`<`、`>`这些字符的时候,应该转义成`HTML`能正确表示这些字符的写法,比如`>`在`HTML`中应该用`<`来表示等。
|
||||
|
||||
但是`Flask`中默认没有开启全局自动转义,针对那些以`.html`、`.htm`、`.xml`和`.xhtml`结尾的文件,如果采用`render_template`函数进行渲染的,则会开启自动转义。并且当用`render_template_string`函数的时候,会将所有的字符串进行转义后再渲染。而对于`Jinja2`默认没有开启全局自动转义,作者有自己的原因:
|
||||
|
||||
1. 渲染到模板中的字符串并不是所有都是危险的,大部分还是没有问题的,如果开启自动转义,那么将会带来大量的不必要的开销。
|
||||
2. `Jinja2`很难获取当前的字符串是否已经被转义过了,因此如果开启自动转义,将对一些已经被转义过的字符串发生二次转义,在渲染后会破坏原来的字符串。
|
||||
|
||||
在没有开启自动转义的模式下(比如以`.conf`结尾的文件),对于一些不信任的字符串,可以通过`{{ content_html|e }}`或者是`{{ content_html|escape }}`的方式进行转义。在开启了自动转义的模式下,如果想关闭自动转义,可以通过`{{ content_html|safe }}`的方式关闭自动转义。而`{%autoescape true/false%}...{%endautoescape%}`可以将一段代码块放在中间,来关闭或开启自动转义,例如以下代码关闭了自动转义:
|
||||
|
||||
```jinja2
|
||||
{% autoescape false %}
|
||||
<p>autoescaping is disabled here
|
||||
<p>{{ will_not_be_escaped }}
|
||||
{% endautoescape %}
|
||||
```
|
||||
|
||||
## 10. 数据类型和运算符
|
||||
|
||||
### 10.1 数据类型:
|
||||
|
||||
`Jinja`支持许多数据类型,包括:**字符串、整型、浮点型、列表、元组、字典、True/False**。
|
||||
|
||||
### 10.2 运算符:
|
||||
|
||||
- `+`号运算符:可以完成数字相加,字符串相加,列表相加。但是并不推荐使用`+`运算符来操作字符串,字符串相加应该使用`~`运算符。
|
||||
- `-`号运算符:只能针对两个数字相减。
|
||||
- `/`号运算符:对两个数进行相除。
|
||||
- `%`号运算符:取余运算。
|
||||
- `*`号运算符:乘号运算符,并且可以对字符进行相乘。
|
||||
- `**`号运算符:次幂运算符,比如2**3=8。
|
||||
- `in`操作符:跟python中的`in`一样使用,比如`{{1 in [1,2,3]}}`返回`true`。
|
||||
- `~`号运算符:拼接多个字符串,比如`{{"Hello" ~ "World"}}`将返回`HelloWorld`。
|
||||
|
||||
## 11. 静态文件的配置
|
||||
|
||||
`Web`应用中会出现大量的静态文件来使得网页更加生动美观。类似于`CSS`样式文件、`JavaScript`脚本文件、图片文件、字体文件等静态资源。在`Jinja`中加载静态文件非常简单,只需要通过`url_for`全局函数就可以实现,看以下代码:
|
||||
|
||||
```html
|
||||
<link href="{{ url_for('static',filename='about.css') }}">
|
||||
```
|
||||
|
||||
`url_for`函数默认会在项目根目录下的`static`文件夹中寻找`about.css`文件,如果找到了,会生成一个相对于项目根目录下的`/static/about.css`路径。当然我们也可以把静态文件不放在`static`文件夹中,此时就需要具体指定了,看以下代码:
|
||||
|
||||
```python
|
||||
app = Flask(__name__,static_folder='C:\static')
|
||||
```
|
||||
|
||||
那么访问静态文件的时候,将会到`/static`这个文件夹下寻找。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
182
04.Flask/03.视图高级.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# 视图高级
|
||||
|
||||
## 1. 类视图
|
||||
|
||||
之前我们接触的视图都是函数,所以一般简称视图函数。其实视图也可以基于类来实现,类视图的好处是支持继承,但是类视图不能跟函数视图一样,写完类视图还需要通过`app.add_url_rule(url_rule,view_func)`来进行注册。以下将对两种类视图进行讲解:
|
||||
|
||||
### 1.1 标准类视图:
|
||||
|
||||
标准类视图是继承自`flask.views.View`,并且在子类中必须实现`dispatch_request`方法,这个方法类似于视图函数,也要返回一个基于`Response`或者其子类的对象。以下将用一个例子进行讲解:
|
||||
|
||||
```python
|
||||
from flask.views import View
|
||||
class PersonalView(View):
|
||||
def dispatch_request(self):
|
||||
return "知了课堂"
|
||||
# 类视图通过add_url_rule方法和url做映射
|
||||
app.add_url_rule('/users/',view_func=PersonalView.as_view('personalview'))
|
||||
```
|
||||
|
||||
### 1.2 基于调度方法的视图:
|
||||
|
||||
`Flask`还为我们提供了另外一种类视图`flask.views.MethodView`,对每个HTTP方法执行不同的函数(映射到对应方法的小写的同名方法上),以下将用一个例子来进行讲解:
|
||||
|
||||
```python
|
||||
class LoginView(views.MethodView):
|
||||
# 当客户端通过get方法进行访问的时候执行的函数
|
||||
def get(self):
|
||||
return render_template("login.html")
|
||||
|
||||
# 当客户端通过post方法进行访问的时候执行的函数
|
||||
def post(self):
|
||||
email = request.form.get("email")
|
||||
password = request.form.get("password")
|
||||
if email == 'xx@qq.com' and password == '111111':
|
||||
return "登录成功!"
|
||||
else:
|
||||
return "用户名或密码错误!"
|
||||
|
||||
## 通过add_url_rule添加类视图和url的映射,并且在as_view方法中指定该url的名称,方便url_for函数调用
|
||||
app.add_url_rule('/myuser/',view_func=LoginView.as_view('loginview'))
|
||||
```
|
||||
|
||||
如果用类视图,我们怎么使用装饰器呢?比如有时候需要做权限验证的时候,比如看以下例子:
|
||||
|
||||
```python
|
||||
from flask import session
|
||||
def login_required(func):
|
||||
def wrapper(*args,**kwargs):
|
||||
if not session.get("user_id"):
|
||||
return 'auth failure'
|
||||
return func(*args,**kwargs)
|
||||
return wrapper
|
||||
```
|
||||
|
||||
装饰器写完后,可以在类视图中定义一个属性叫做`decorators`,然后存储装饰器。以后每次调用这个类视图的时候,就会执行这个装饰器。示例代码如下:
|
||||
|
||||
```python
|
||||
class UserView(views.MethodView):
|
||||
decorators = [user_required]
|
||||
...
|
||||
```
|
||||
|
||||
## 2. 蓝图和子域名
|
||||
|
||||
### 2.1 蓝图:
|
||||
|
||||
之前我们写的`url`和视图函数都是处在同一个文件,如果项目比较大的话,这显然不是一个合理的结构,而蓝图可以优雅的帮我们实现这种需求。以下看一个使用蓝图的文件的例子:
|
||||
|
||||
```
|
||||
from flask import Blueprint
|
||||
bp = Blueprint('user',__name__,url_prefix='/user/')
|
||||
|
||||
@bp.route('/')
|
||||
def index():
|
||||
return "用户首页"
|
||||
|
||||
@bp.route('profile/')
|
||||
def profile():
|
||||
return "个人简介"
|
||||
```
|
||||
|
||||
然后我们在主程序中,通过`app.register_blueprint()`方法将这个蓝图注册进url映射中,看下主`app`的实现:
|
||||
|
||||
```
|
||||
from flask import Flask
|
||||
import user
|
||||
|
||||
app = Flask(__name__)
|
||||
app.register_blueprint(user.bp)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0',port=9000)
|
||||
```
|
||||
|
||||
以后访问`/user/`,`/user/profile/`,都是执行的`user.py`文件中的视图函数,这样就实现了项目的模块化。
|
||||
|
||||
以上是对蓝图的一个简单介绍,但是使用蓝图还有几个需要注意的地方,就是在蓝图如何寻找静态文件、模板文件,`url_for`函数如何反转`url`,以下分别进行解释:
|
||||
|
||||
#### 2.1.1 寻找静态文件:
|
||||
|
||||
默认不设置任何静态文件路径,`Jinja2`会在项目的`static`文件夹中寻找静态文件。也可以设置其他的路径,在初始化蓝图的时候,`Blueprint`这个构造函数,有一个参数`static_folder`可以指定静态文件的路径,如:
|
||||
|
||||
```python
|
||||
bp = Blueprint('admin',__name__,url_prefix='/admin',static_folder='static')
|
||||
```
|
||||
|
||||
`static_folder`可以是相对路径(相对蓝图文件所在的目录),也可以是绝对路径。在配置完蓝图后,还有一个需要注意的地方是如何在模板中引用静态文件。在模板中引用蓝图,应该要使用`蓝图名+.+static`来引用,如下所示:
|
||||
|
||||
```python
|
||||
<link href="{{ url_for('admin.static',filename='about.css') }}">
|
||||
```
|
||||
|
||||
#### 2.1.2 寻找模板文件:
|
||||
|
||||
跟静态文件一样,默认不设置任何模板文件的路径,将会在项目的`templates`中寻找模板文件。也可以设置其他的路径,在构造函数`Blueprint`中有一个`template_folder`参数可以设置模板的路径,如下所示:
|
||||
|
||||
```python
|
||||
bp = Blueprint('admin',__name__,url_prefix='/admin',template_folder='templates')
|
||||
```
|
||||
|
||||
模板文件和静态文件有点区别,以上代码写完以后,如果你渲染一个模板`return render_template('admin.html')`,`Flask`默认会去项目根目录下的`templates`文件夹中查找`admin.html`文件,如果找到了就直接返回,如果没有找到,才会去蓝图文件所在的目录下的`templates`文件夹中寻找。
|
||||
|
||||
#### 2.1.3 url_for生成`url`:
|
||||
|
||||
用`url_for`生成蓝图的`url`,使用的格式是:`蓝图名称+.+视图函数名称`。比如要获取`admin`这个蓝图下的`index`视图函数的`url`,应该采用以下方式:
|
||||
|
||||
```python
|
||||
url_for('admin.index')
|
||||
```
|
||||
|
||||
其中这个**蓝图名称**是在创建蓝图的时候,传入的第一个参数。`bp = Blueprint('admin',__name__,url_prefix='/admin',template_folder='templates')`
|
||||
|
||||
### 2.2 子域名:
|
||||
|
||||
子域名在许多网站中都用到了,比如一个网站叫做`xxx.com`,那么我们可以定义一个子域名`cms.xxx.com`来作为`cms`管理系统的网址,子域名的实现一般也是通过蓝图来实现,在之前章节中,我们创建蓝图的时候添加了一个`url_prefix=/user`作为url前缀,那样我们就可以通过`/user/`来访问`user`下的url。但使用子域名则不需要。另外,还需要配置`SERVER_NAME`,比如`app.config[SERVER_NAME]='example.com:9000'`。并且在注册蓝图的时候,还需要添加一个`subdomain`的参数,这个参数就是子域名的名称,先来看一下蓝图的实现(admin.py):
|
||||
|
||||
```python
|
||||
from flask import Blueprint
|
||||
bp = Blueprint('admin',__name__,subdomain='admin')
|
||||
|
||||
@bp.route('/')
|
||||
def admin():
|
||||
return 'Admin Page'
|
||||
```
|
||||
|
||||
这个没有多大区别,接下来看主`app`的实现:
|
||||
|
||||
```python
|
||||
from flask import Flask
|
||||
import admin
|
||||
|
||||
# 配置`SERVER_NAME`
|
||||
app.config['SERVER_NAME'] = 'example.com:8000'
|
||||
# 注册蓝图,指定了subdomain
|
||||
app.register_blueprint(admin.bp)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0',port=8000,debug=True)
|
||||
```
|
||||
|
||||
写完以上两个文件后,还是不能正常的访问`admin.example.com:8000`这个子域名,因为我们没有在`host`文件中添加域名解析,你可以在最后添加一行`127.0.0.1 admin.example.com`,就可以访问到了。另外,子域名不能在`127.0.0.1`上出现,也不能在`localhost`上出现。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
919
04.Flask/04.SQLAlchemy.md
Normal file
@@ -0,0 +1,919 @@
|
||||
# SQLAlchemy
|
||||
|
||||
## 1. MySQL数据库
|
||||
|
||||
在网站开发中,数据库是网站的重要组成部分。只有提供数据库,数据才能够动态的展示,而不是在网页中显示一个静态的页面。数据库有很多,比如有`SQL Server`、`Oracle`、`PostgreSQL`以及`MySQL`等等。`MySQL`由于价格实惠、简单易用、不受平台限制、灵活度高等特性,目前已经取得了绝大多数的市场份额。因此我们在`Flask`中,也是使用`MySQL`来作为数据存储。
|
||||
|
||||
### 1.1 MySQL数据库安装
|
||||
|
||||
1. 在`MySQL`的官网下载`MySQL`数据库安装文件:`https://dev.mysql.com/downloads/mysql/`。
|
||||
2. 然后双击安装,如果出现以下错误,则到`http://www.microsoft.com/en-us/download/details.aspx?id=17113`下载`.net framework`。
|
||||

|
||||
3. 在安装过程中,如果提示没有`Microsoft C++ 2013`,那么就到以下网址下载安装即可:`http://download.microsoft.com/download/9/0/5/905DBD86-D1B8-4D4B-8A50-CB0E922017B9/vcredist_x64.exe`。
|
||||

|
||||
4. 接下来就是做好用户名和密码的配置即可。
|
||||
|
||||
### 1.2 navicat数据库操作软件
|
||||
|
||||
安装完`MySQL`数据库以后,就可以使用`MySQL`提供的终端客户端软件来操作数据库。如下:
|
||||

|
||||
这个软件所有的操作都是基于`sql`语言,对于想要熟练`sql`语言的同学来讲是非常合适的。但是对于在企业中可能不是一款好用的工具。在企业中我们推荐使用`mysql workbench`以及`navicat`这种图形化操作的软件。而`mysql workbench`是`mysql`官方提供的一个免费的软件,正因为是免费,所以在一些功能上不及`navicat`。`navicat for mysql`是一款收费的软件。官网地址如下:`https://www.navicat.com.cn/products`。使用的截图如下:
|
||||

|
||||
|
||||
### 1.3 MySQL驱动程序安装
|
||||
|
||||
我们使用`Django`来操作`MySQL`,实际上底层还是通过`Python`来操作的。因此我们想要用`Flask`来操作`MySQL`,首先还是需要安装一个驱动程序。在`Python3`中,驱动程序有多种选择。比如有`pymysql`以及`mysqlclient`等。这里我们就使用`mysqlclient`来操作。`mysqlclient`安装非常简单。只需要通过`pip install mysqlclient`即可安装。
|
||||
|
||||
常见`MySQL`驱动介绍:
|
||||
|
||||
1. `MySQL-python`:也就是`MySQLdb`。是对`C`语言操作`MySQL`数据库的一个简单封装。遵循了`Python DB API v2`。但是只支持`Python2`,目前还不支持`Python3`。
|
||||
2. `mysqlclient`:是`MySQL-python`的另外一个分支。支持`Python3`并且修复了一些`bug`。
|
||||
3. `pymysql`:纯`Python`实现的一个驱动。因为是纯`Python`编写的,因此执行效率不如`MySQL-python`。并且也因为是纯`Python`编写的,因此可以和`Python`代码无缝衔接。
|
||||
4. `MySQL Connector/Python`:`MySQL`官方推出的使用纯`Python`连接`MySQL`的驱动。因为是纯`Python`开发的。效率不高。
|
||||
|
||||
## 2. SQLAlchemy介绍
|
||||
|
||||
### 2.1 安装:
|
||||
|
||||
`SQLAlchemy`是一个数据库的`ORM`框架,让我们操作数据库的时候不要再用`SQL`语句了,跟直接操作模型一样。安装命令为:`pip install SQLAlchemy`。
|
||||
|
||||
### 2.2 通过`SQLAlchemy`连接数据库:
|
||||
|
||||
首先来看一段代码:
|
||||
|
||||
```python
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
# 数据库的配置变量
|
||||
HOSTNAME = '127.0.0.1'
|
||||
PORT = '3306'
|
||||
DATABASE = 'xt_flask'
|
||||
USERNAME = 'root'
|
||||
PASSWORD = 'root'
|
||||
DB_URI = 'mysql+pymysql://{}:{}@{}:{}/{}'.format(USERNAME,PASSWORD,HOSTNAME,PORT,DATABASE)
|
||||
|
||||
# 创建数据库引擎
|
||||
engine = create_engine(DB_URI)
|
||||
|
||||
#创建连接
|
||||
with engine.connect() as con:
|
||||
rs = con.execute('SELECT 1')
|
||||
print rs.fetchone()
|
||||
```
|
||||
|
||||
首先从`sqlalchemy`中导入`create_engine`,用这个函数来创建引擎,然后用`engine.connect()`来连接数据库。其中一个比较重要的一点是,通过`create_engine`函数的时候,需要传递一个满足某种格式的字符串,对这个字符串的格式来进行解释:
|
||||
|
||||
```
|
||||
dialect+driver://username:password@host:port/database?charset=utf8
|
||||
```
|
||||
|
||||
`dialect`是数据库的实现,比如`MySQL`、`PostgreSQL`、`SQLite`,并且转换成小写。`driver`是`Python`对应的驱动,如果不指定,会选择默认的驱动,比如MySQL的默认驱动是`MySQLdb`。`username`是连接数据库的用户名,`password`是连接数据库的密码,`host`是连接数据库的域名,`port`是数据库监听的端口号,`database`是连接哪个数据库的名字。
|
||||
|
||||
如果以上输出了`1`,说明`SQLAlchemy`能成功连接到数据库。
|
||||
|
||||
### 2.3 用SQLAlchemy执行原生SQL:
|
||||
|
||||
我们将上一个例子中的数据库配置选项单独放在一个`constants.py`的文件中,看以下例子:
|
||||
|
||||
```python
|
||||
from sqlalchemy import create_engine
|
||||
from constants import DB_URI
|
||||
|
||||
#连接数据库
|
||||
engine = create_engine(DB_URI,echo=True)
|
||||
|
||||
# 使用with语句连接数据库,如果发生异常会被捕获
|
||||
with engine.connect() as con:
|
||||
# 先删除users表
|
||||
con.execute('drop table if exists authors')
|
||||
# 创建一个users表,有自增长的id和name
|
||||
con.execute('create table authors(id int primary key auto_increment,'name varchar(25))')
|
||||
# 插入两条数据到表中
|
||||
con.execute('insert into persons(name) values("abc")')
|
||||
con.execute('insert into persons(name) values("xiaotuo")')
|
||||
# 执行查询操作
|
||||
results = con.execute('select * from persons')
|
||||
# 从查找的结果中遍历
|
||||
for result in results:
|
||||
print(result)
|
||||
```
|
||||
|
||||
## 3. SQLAlchemy基本使用
|
||||
|
||||
### 3.1 使用SQLAlchemy:
|
||||
|
||||
#### 3.1.1 创建`ORM`模型:
|
||||
|
||||
要使用`ORM`来操作数据库,首先需要创建一个类来与对应的表进行映射。现在以`User表`来做为例子,它有`自增长的id`、`name`、`fullname`、`password`这些字段,那么对应的类为:
|
||||
|
||||
```python
|
||||
from sqlalchemy import Column,Integer,String
|
||||
from constants import DB_URI
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
engine = create_engine(DB_URI,echo=True)
|
||||
|
||||
# 所有的类都要继承自`declarative_base`这个函数生成的基类
|
||||
Base = declarative_base(engine)
|
||||
class User(Base):
|
||||
# 定义表名为users
|
||||
__tablename__ = 'users'
|
||||
|
||||
# 将id设置为主键,并且默认是自增长的
|
||||
id = Column(Integer,primary_key=True)
|
||||
# name字段,字符类型,最大的长度是50个字符
|
||||
name = Column(String(50))
|
||||
fullname = Column(String(50))
|
||||
password = Column(String(100))
|
||||
|
||||
# 让打印出来的数据更好看,可选的
|
||||
def __repr__(self):
|
||||
return "<User(id='%s',name='%s',fullname='%s',password='%s')>" % (self.id,self.name,self.fullname,self.password)
|
||||
```
|
||||
|
||||
#### 3.1.2 映射到数据库中:
|
||||
|
||||
`SQLAlchemy`会自动的设置第一个`Integer`的主键并且没有被标记为外键的字段添加自增长的属性。因此以上例子中`id`自动的变成自增长的。以上创建完和表映射的类后,还没有真正的映射到数据库当中,执行以下代码将类映射到数据库中:
|
||||
|
||||
```python
|
||||
Base.metadata.create_all()
|
||||
```
|
||||
|
||||
#### 3.1.3 添加数据到表中:
|
||||
|
||||
在创建完数据表,并且做完和数据库的映射后,接下来让我们添加数据进去:
|
||||
|
||||
```python
|
||||
ed_user = User(name='ed',fullname='Ed Jones',password='edspassword')
|
||||
# 打印名字
|
||||
print ed_user.name
|
||||
> ed
|
||||
# 打印密码
|
||||
print ed_user.password
|
||||
> edspassword
|
||||
# 打印id
|
||||
print(ed_user.id)
|
||||
> None
|
||||
```
|
||||
|
||||
可以看到,name和password都能正常的打印,唯独`id`为`None`,这是因为`id`是一个自增长的主键,还未插入到数据库中,`id`是不存在的。接下来让我们把创建的数据插入到数据库中。和数据库打交道的,是一个叫做`Session`的对象:
|
||||
|
||||
```python
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
Session = sessionmaker(bind=engine)
|
||||
# 或者
|
||||
# Session = sessionmaker()
|
||||
# Session.configure(bind=engine)
|
||||
session = Session()
|
||||
ed_user = User(name='ed',fullname='Ed Jones',password='edspassword')
|
||||
session.add(ed_user)
|
||||
```
|
||||
|
||||
现在只是把数据添加到`session`中,但是并没有真正的把数据存储到数据库中。如果需要把数据存储到数据库中,还要做一次`commit`操作:
|
||||
|
||||
```python
|
||||
session.commit()
|
||||
# 打印ed_user的id
|
||||
print ed_user.id
|
||||
> 1
|
||||
```
|
||||
|
||||
#### 3.1.4 回滚:
|
||||
|
||||
这时候,`ed_user`就已经有id。 说明已经插入到数据库中了。有人肯定有疑问了,为什么添加到`session`中后还要做一次`commit`操作呢,这是因为,在`SQLAlchemy`的`ORM`实现中,在做`commit`操作之前,所有的操作都是在事务中进行的,因此如果你要将事务中的操作真正的映射到数据库中,还需要做`commit`操作。既然用到了事务,这里就并不能避免的提到一个回滚操作了,那么看以下代码展示了如何使用回滚(接着以上示例代码):
|
||||
|
||||
```python
|
||||
# 修改ed_user的用户名
|
||||
ed_user.name = 'Edwardo'
|
||||
# 创建一个新的用户
|
||||
fake_user = User(name='fakeuser',fullname='Invalid',password='12345')
|
||||
# 将新创建的fake_user添加到session中
|
||||
session.add(fake_user)
|
||||
# 判断`fake_user`是否在`session`中存在
|
||||
print fake_user in session
|
||||
> True
|
||||
# 从数据库中查找name=Edwardo的用户
|
||||
tmp_user = session.query(User).filter_by(name='Edwardo')
|
||||
# 打印tmp_user的name
|
||||
print tmp_user
|
||||
# 打印出查找到的tmp_user对象,注意这个对象的name属性已经在事务中被修改为Edwardo了。
|
||||
> <User(name='Edwardo', fullname='Ed Jones', password='edspassword')>
|
||||
# 刚刚所有的操作都是在事务中进行的,现在来做回滚操作
|
||||
session.rollback()
|
||||
# 再打印tmp_user
|
||||
print tmp_user
|
||||
> <User(name='ed', fullname='Ed Jones', password='edspassword')>
|
||||
# 再看fake_user是否还在session中
|
||||
print fake_user in session
|
||||
> False
|
||||
```
|
||||
|
||||
#### 3.1.5 查找数据:
|
||||
|
||||
接下来看下如何进行查找操作,查找操作是通过`session.query()`方法实现的,这个方法会返回一个`Query`对象,`Query`对象相当于一个数组,装载了查找出来的数据,并且可以进行迭代。具体里面装的什么数据,就要看向`session.query()`方法传的什么参数了,如果只是传一个`ORM`的类名作为参数,那么提取出来的数据就是都是这个类的实例,比如:
|
||||
|
||||
```python
|
||||
for instance in session.query(User).order_by(User.id):
|
||||
print instance
|
||||
# 输出所有的user实例
|
||||
> <User (id=2,name='ed',fullname='Ed Json',password='12345')>
|
||||
> <User (id=3,name='be',fullname='Be Engine',password='123456')>
|
||||
```
|
||||
|
||||
如果传递了两个及其两个以上的对象,或者是传递的是`ORM`类的属性,那么查找出来的就是元组,例如:
|
||||
|
||||
```python
|
||||
for instance in session.query(User.name):
|
||||
print instance
|
||||
# 输出所有的查找结果
|
||||
> ('ed',)
|
||||
> ('be',)
|
||||
```
|
||||
|
||||
以及:
|
||||
|
||||
```python
|
||||
for instance in session.query(User.name,User.fullname):
|
||||
print instance
|
||||
# 输出所有的查找结果
|
||||
> ('ed', 'Ed Json')
|
||||
> ('be', 'Be Engine')
|
||||
```
|
||||
|
||||
或者是:
|
||||
|
||||
```python
|
||||
for instance in session.query(User,User.name).all():
|
||||
print instance
|
||||
# 输出所有的查找结果
|
||||
> (<User (id=2,name='ed',fullname='Ed Json',password='12345')>, 'Ed Json')
|
||||
> (<User (id=3,name='be',fullname='Be Engine',password='123456')>, 'Be Engine')
|
||||
```
|
||||
|
||||
另外,还可以对查找的结果(`Query`)做切片操作:
|
||||
|
||||
```python
|
||||
for instance in session.query(User).order_by(User.id)[1:3]
|
||||
instance
|
||||
```
|
||||
|
||||
如果想对结果进行过滤,可以使用`filter_by`和`filter`两个方法,这两个方法都是用来做过滤的,区别在于,`filter_by`是传入关键字参数,`filter`是传入条件判断,并且`filter`能够传入的条件更多更灵活,请看以下例子:
|
||||
|
||||
```python
|
||||
# 第一种:使用filter_by过滤:
|
||||
for name in session.query(User.name).filter_by(fullname='Ed Jones'):
|
||||
print name
|
||||
# 输出结果:
|
||||
> ('ed',)
|
||||
|
||||
# 第二种:使用filter过滤:
|
||||
for name in session.query(User.name).filter(User.fullname=='Ed Jones'):
|
||||
print name
|
||||
# 输出结果:
|
||||
> ('ed',)
|
||||
```
|
||||
|
||||
### 3.2 Column常用参数:
|
||||
|
||||
- `default`:默认值。
|
||||
- `nullable`:是否可空。
|
||||
- `primary_key`:是否为主键。
|
||||
- `unique`:是否唯一。
|
||||
- `autoincrement`:是否自动增长。
|
||||
- `onupdate`:更新的时候执行的函数。
|
||||
- `name`:该属性在数据库中的字段映射。
|
||||
|
||||
### 3.3 sqlalchemy常用数据类型:
|
||||
|
||||
- `Integer`:整形。
|
||||
- `Float`:浮点类型。
|
||||
- `Boolean`:传递`True/False`进去。
|
||||
- `DECIMAL`:定点类型。
|
||||
- `enum`:枚举类型。
|
||||
- `Date`:传递`datetime.date()`进去。
|
||||
- `DateTime`:传递`datetime.datetime()`进去。
|
||||
- `Time`:传递`datetime.time()`进去。
|
||||
- `String`:字符类型,使用时需要指定长度,区别于`Text`类型。
|
||||
- `Text`:文本类型。
|
||||
- `LONGTEXT`:长文本类型。
|
||||
|
||||
## 4. 查找操作
|
||||
|
||||
### 4.1 query可用参数:
|
||||
|
||||
1. 模型对象。指定查找这个模型中所有的对象。
|
||||
2. 模型中的属性。可以指定只查找某个模型的其中几个属性。
|
||||
3. 聚合函数。
|
||||
- `func.count`:统计行的数量。
|
||||
- `func.avg`:求平均值。
|
||||
- `func.max`:求最大值。
|
||||
- `func.min`:求最小值。
|
||||
- `func.sum`:求和。
|
||||
|
||||
### 4.2 过滤条件:
|
||||
|
||||
过滤是数据提取的一个很重要的功能,以下对一些常用的过滤条件进行解释,并且这些过滤条件都是只能通过`filter`方法实现的:
|
||||
|
||||
1. `equals`:
|
||||
|
||||
```python
|
||||
query.filter(User.name == 'ed')
|
||||
```
|
||||
|
||||
2. `not equals`:
|
||||
|
||||
```python
|
||||
query.filter(User.name != 'ed')
|
||||
```
|
||||
|
||||
3. `like`:
|
||||
|
||||
```python
|
||||
query.filter(User.name.like('%ed%'))
|
||||
```
|
||||
|
||||
4. `in`:
|
||||
|
||||
```python
|
||||
query.filter(User.name.in_(['ed','wendy','jack']))
|
||||
# 同时,in也可以作用于一个Query
|
||||
query.filter(User.name.in_(session.query(User.name).filter(User.name.like('%ed%'))))
|
||||
```
|
||||
|
||||
5. `not in`:
|
||||
|
||||
```python
|
||||
query.filter(~User.name.in_(['ed','wendy','jack']))
|
||||
```
|
||||
|
||||
6. `is null`:
|
||||
|
||||
```python
|
||||
query.filter(User.name==None)
|
||||
# 或者是
|
||||
query.filter(User.name.is_(None))
|
||||
```
|
||||
|
||||
7. `is not null`:
|
||||
|
||||
```python
|
||||
query.filter(User.name != None)
|
||||
# 或者是
|
||||
query.filter(User.name.isnot(None))
|
||||
```
|
||||
|
||||
8. `and`:
|
||||
|
||||
```python
|
||||
from sqlalchemy import and_
|
||||
query.filter(and_(User.name=='ed',User.fullname=='Ed Jones'))
|
||||
# 或者是传递多个参数
|
||||
query.filter(User.name=='ed',User.fullname=='Ed Jones')
|
||||
# 或者是通过多次filter操作
|
||||
query.filter(User.name=='ed').filter(User.fullname=='Ed Jones')
|
||||
```
|
||||
|
||||
9. `or`:
|
||||
|
||||
```python
|
||||
from sqlalchemy import or_ query.filter(or_(User.name=='ed',User.name=='wendy'))
|
||||
```
|
||||
|
||||
### 4.3 查找方法:
|
||||
|
||||
介绍完过滤条件后,有一些经常用到的查找数据的方法也需要解释一下:
|
||||
|
||||
1. `all()`:返回一个`Python`列表(`list`):
|
||||
|
||||
```python
|
||||
query = session.query(User).filter(User.name.like('%ed%').order_by(User.id)
|
||||
# 输出query的类型
|
||||
print type(query)
|
||||
> <type 'list'>
|
||||
# 调用all方法
|
||||
query = query.all()
|
||||
# 输出query的类型
|
||||
print type(query)
|
||||
> <class 'sqlalchemy.orm.query.Query'>
|
||||
```
|
||||
|
||||
2. `first()`:返回`Query`中的第一个值:
|
||||
|
||||
```python
|
||||
user = session.query(User).first()
|
||||
print user
|
||||
> <User(name='ed', fullname='Ed Jones', password='f8s7ccs')>
|
||||
```
|
||||
|
||||
3. `one()`:查找所有行作为一个结果集,如果结果集中只有一条数据,则会把这条数据提取出来,如果这个结果集少于或者多于一条数据,则会抛出异常。总结一句话:有且只有一条数据的时候才会正常的返回,否则抛出异常:
|
||||
|
||||
```python
|
||||
# 多于一条数据
|
||||
user = query.one()
|
||||
> Traceback (most recent call last):
|
||||
> ...
|
||||
> MultipleResultsFound: Multiple rows were found for one()
|
||||
# 少于一条数据
|
||||
user = query.filter(User.id == 99).one()
|
||||
> Traceback (most recent call last):
|
||||
> ...
|
||||
> NoResultFound: No row was found for one()
|
||||
# 只有一条数据
|
||||
> query(User).filter_by(name='ed').one()
|
||||
```
|
||||
|
||||
4. `one_or_none()`:跟`one()`方法类似,但是在结果集中没有数据的时候也不会抛出异常。
|
||||
|
||||
5. `scalar()`:底层调用`one()`方法,并且如果`one()`方法没有抛出异常,会返回查询到的第一列的数据:
|
||||
|
||||
```python
|
||||
session.query(User.name,User.fullname).filter_by(name='ed').scalar()
|
||||
```
|
||||
|
||||
### 4.4 文本SQL:
|
||||
|
||||
`SQLAlchemy`还提供了使用**文本SQL**的方式来进行查询,这种方式更加的灵活。而文本SQL要装在一个`text()`方法中,看以下例子:
|
||||
|
||||
```python
|
||||
from sqlalchemy import text
|
||||
for user in session.query(User).filter(text("id<244")).order_by(text("id")).all():
|
||||
print user.name
|
||||
```
|
||||
|
||||
如果过滤条件比如上例中的244存储在变量中,这时候就可以通过传递参数的形式进行构造:
|
||||
|
||||
```python
|
||||
session.query(User).filter(text("id<:value and name=:name")).params(value=224,name='ed').order_by(User.id)
|
||||
```
|
||||
|
||||
在文本SQL中的变量前面使用了`:`来区分,然后使用`params`方法,指定需要传入进去的参数。另外,使用`from_statement`方法可以把过滤的函数和条件函数都给去掉,使用纯文本的SQL:
|
||||
|
||||
```python
|
||||
sesseion.query(User).from_statement(text("select * from users where name=:name")).params(name='ed').all()
|
||||
```
|
||||
|
||||
使用`from_statement`方法一定要注意,`from_statement`返回的是一个`text`里面的查询语句,一定要记得调用`all()`方法来获取所有的值。
|
||||
|
||||
### 4.5 计数(Count):
|
||||
|
||||
`Query`对象有一个非常方便的方法来计算里面装了多少数据:
|
||||
|
||||
```python
|
||||
session.query(User).filter(User.name.like('%ed%')).count()
|
||||
```
|
||||
|
||||
当然,有时候你想明确的计数,比如要统计`users`表中有多少个不同的姓名,那么简单粗暴的采用以上`count`是不行的,因为姓名有可能会重复,但是处于两条不同的数据上,如果在原生数据库中,可以使用`distinct`关键字,那么在`SQLAlchemy`中,可以通过`func.count()`方法来实现:
|
||||
|
||||
```python
|
||||
from sqlalchemy import func
|
||||
session.query(func.count(User.name),User.name).group_by(User.name).all()
|
||||
## 输出的结果
|
||||
> [(1, u'ed'), (1, u'fred'), (1, u'mary'), (1, u'wendy')]
|
||||
```
|
||||
|
||||
另外,如果想实现`select count(*) from users`,可以通过以下方式来实现:
|
||||
|
||||
```python
|
||||
session.query(func.count(*)).select_from(User).scalar()
|
||||
```
|
||||
|
||||
当然,如果指定了要查找的表的字段,可以省略`select_from()`方法:
|
||||
|
||||
```python
|
||||
session.query(func.count(User.id)).scalar()
|
||||
```
|
||||
|
||||
## 5. 表关系:
|
||||
|
||||
表之间的关系存在三种:一对一、一对多、多对多。而`SQLAlchemy`中的`ORM`也可以模拟这三种关系。因为一对一其实在`SQLAlchemy`中底层是通过一对多的方式模拟的,所以先来看下一对多的关系:
|
||||
|
||||
### 5.1 外键:
|
||||
|
||||
在Mysql中,外键可以让表之间的关系更加紧密。而SQLAlchemy同样也支持外键。通过ForeignKey类来实现,并且可以指定表的外键约束。相关示例代码如下:
|
||||
|
||||
```python
|
||||
class Article(Base):
|
||||
__tablename__ = 'article'
|
||||
id = Column(Integer,primary_key=True,autoincrement=True)
|
||||
title = Column(String(50),nullable=False)
|
||||
content = Column(Text,nullable=False)
|
||||
uid = Column(Integer,ForeignKey('user.id'))
|
||||
|
||||
def __repr__(self):
|
||||
return "<Article(title:%s)>" % self.title
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = 'user'
|
||||
id = Column(Integer,primary_key=True,autoincrement=True)
|
||||
username = Column(String(50),nullable=False)
|
||||
```
|
||||
|
||||
外键约束有以下几项:
|
||||
|
||||
1. `RESTRICT`:父表数据被删除,会阻止删除。默认就是这一项。
|
||||
2. `NO ACTION`:在MySQL中,同`RESTRICT`。
|
||||
3. `CASCADE`:级联删除。
|
||||
4. `SET NULL`:父表数据被删除,子表数据会设置为NULL。
|
||||
|
||||
### 5.2 一对多:
|
||||
|
||||
拿之前的`User`表为例,假如现在要添加一个功能,要保存用户的邮箱帐号,并且邮箱帐号可以有多个,这时候就必须创建一个新的表,用来存储用户的邮箱,然后通过`user.id`来作为外键进行引用,先来看下邮箱表的实现:
|
||||
|
||||
```python
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
class Address(Base):
|
||||
__tablename__ = 'address'
|
||||
id = Column(Integer,primary_key=True)
|
||||
email_address = Column(String,nullable=False)
|
||||
# User表的外键,指定外键的时候,是使用的是数据库表的名称,而不是类名
|
||||
user_id = Column(Integer,ForeignKey('users.id'))
|
||||
# 在ORM层面绑定两者之间的关系,第一个参数是绑定的表的类名,
|
||||
# 第二个参数back_populates是通过User反向访问时的字段名称
|
||||
user = relationship('User',back_populates="addresses")
|
||||
|
||||
def __repr__(self):
|
||||
return "<Address(email_address='%s')>" % self.email_address
|
||||
|
||||
# 重新修改User表,添加了addresses字段,引用了Address表的主键
|
||||
class User(Base):
|
||||
__tablename__ = 'users'
|
||||
id = Column(Integer,primary_key=True)
|
||||
name = Column(String(50))
|
||||
fullname = Column(String(50))
|
||||
password = Column(String(100))
|
||||
# 在ORM层面绑定和`Address`表的关系
|
||||
addresses = relationship("Address",order_by=Address.id,back_populates="user")
|
||||
```
|
||||
|
||||
其中,在`User`表中添加的`addresses`字段,可以通过`User.addresses`来访问和这个user相关的所有address。在`Address`表中的`user`字段,可以通过`Address.user`来访问这个user。达到了双向绑定。表关系已经建立好以后,接下来就应该对其进行操作,先看以下代码:
|
||||
|
||||
```python
|
||||
jack = User(name='jack',fullname='Jack Bean',password='gjffdd')
|
||||
jack.addresses = [Address(email_address='jack@google.com'),
|
||||
Address(email_address='j25@yahoo.com')]
|
||||
session.add(jack)
|
||||
session.commit()
|
||||
```
|
||||
|
||||
首先,创建一个用户,然后对这个`jack`用户添加两个邮箱,最后再提交到数据库当中,可以看到这里操作`Address`并没有直接进行保存,而是先添加到用户里面,再保存。
|
||||
|
||||
### 5.3 一对一:
|
||||
|
||||
一对一其实就是一对多的特殊情况,从以上的一对多例子中不难发现,一对应的是`User`表,而多对应的是`Address`,也就是说一个`User`对象有多个`Address`。因此要将一对多转换成一对一,只要设置一个`User`对象对应一个`Address`对象即可,看以下示例:
|
||||
|
||||
```python
|
||||
class User(Base):
|
||||
__tablename__ = 'users'
|
||||
id = Column(Integer,primary_key=True)
|
||||
name = Column(String(50))
|
||||
fullname = Column(String(50))
|
||||
password = Column(String(100))
|
||||
# 设置uselist关键字参数为False
|
||||
addresses = relationship("Address",back_populates='addresses',uselist=False)
|
||||
class Address(Base):
|
||||
__tablename__ = 'addresses'
|
||||
id = Column(Integer,primary_key=True)
|
||||
email_address = Column(String(50))
|
||||
user_id = Column(Integer,ForeignKey('users.id')
|
||||
user = relationship('Address',back_populates='user')
|
||||
```
|
||||
|
||||
从以上例子可以看到,只要在`User`表中的`addresses`字段上添加`uselist=False`就可以达到一对一的效果。设置了一对一的效果后,就不能添加多个邮箱到`user.addresses`字段了,只能添加一个:
|
||||
|
||||
```python
|
||||
user.addresses = Address(email_address='ed@google.com')
|
||||
```
|
||||
|
||||
### 5.4 多对多:
|
||||
|
||||
多对多需要一个中间表来作为连接,同理在`sqlalchemy`中的`orm`也需要一个中间表。假如现在有一个`Teacher`和一个`Classes`表,即老师和班级,一个老师可以教多个班级,一个班级有多个老师,是一种典型的多对多的关系,那么通过`sqlalchemy`的`ORM`的实现方式如下:
|
||||
|
||||
```python
|
||||
association_table = Table('teacher_classes',Base.metadata,
|
||||
Column('teacher_id',Integer,ForeignKey('teacher.id')),
|
||||
Column('classes_id',Integer,ForeignKey('classes.id'))
|
||||
)
|
||||
|
||||
class Teacher(Base):
|
||||
__tablename__ = 'teacher'
|
||||
id = Column(Integer,primary_key=True)
|
||||
tno = Column(String(10))
|
||||
name = Column(String(50))
|
||||
age = Column(Integer)
|
||||
classes = relationship('Classes',secondary=association_table,back_populates='teachers')
|
||||
|
||||
class Classes(Base):
|
||||
__tablename__ = 'classes'
|
||||
id = Column(Integer,primary_key=True)
|
||||
cno = Column(String(10))
|
||||
name = Column(String(50))
|
||||
teachers = relationship('Teacher',secondary=association_table,back_populates='classes')
|
||||
```
|
||||
|
||||
要创建一个多对多的关系表,首先需要一个中间表,通过`Table`来创建一个中间表。上例中第一个参数`teacher_classes`代表的是中间表的表名,第二个参数是`Base`的元类,第三个和第四个参数就是要连接的两个表,其中`Column`第一个参数是表示的是连接表的外键名,第二个参数表示这个外键的类型,第三个参数表示要外键的表名和字段。
|
||||
创建完中间表以后,还需要在两个表中进行绑定,比如在`Teacher`中有一个`classes`属性,来绑定`Classes`表,并且通过`secondary`参数来连接中间表。同理,`Classes`表连接`Teacher`表也是如此。定义完类后,之后就是添加数据,请看以下示例:
|
||||
|
||||
```python
|
||||
teacher1 = Teacher(tno='t1111',name='xiaotuo',age=10)
|
||||
teacher2 = Teacher(tno='t2222',name='datuo',age=10)
|
||||
classes1 = Classes(cno='c1111',name='english')
|
||||
classes2 = Classes(cno='c2222',name='math')
|
||||
teacher1.classes = [classes1,classes2]
|
||||
teacher2.classes = [classes1,classes2]
|
||||
classes1.teachers = [teacher1,teacher2]
|
||||
classes2.teachers = [teacher1,teacher2]
|
||||
session.add(teacher1)
|
||||
session.add(teacher2)
|
||||
session.add(classes1)
|
||||
session.add(classes2)
|
||||
```
|
||||
|
||||
### 5.5 `ORM`层面的`CASCADE`:
|
||||
|
||||
如果将数据库的外键设置为`RESTRICT`,那么在`ORM`层面,删除了父表中的数据,那么从表中的数据将会`NULL`。如果不想要这种情况发生,那么应该将这个值的`nullable=False`。
|
||||
|
||||
在`SQLAlchemy`,只要将一个数据添加到`session`中,和他相关联的数据都可以一起存入到数据库中了。这些是怎么设置的呢?其实是通过`relationship`的时候,有一个关键字参数`cascade`可以设置这些属性:
|
||||
|
||||
1. `save-update`:默认选项。在添加一条数据的时候,会把其他和他相关联的数据都添加到数据库中。这种行为就是`save-update`属性影响的。
|
||||
2. `delete`:表示当删除某一个模型中的数据的时候,是否也删掉使用`relationship`和他关联的数据。
|
||||
3. `delete-orphan`:表示当对一个ORM对象解除了父表中的关联对象的时候,自己便会被删除掉。当然如果父表中的数据被删除,自己也会被删除。这个选项只能用在一对多上,不能用在多对多以及多对一上。并且还需要在子模型中的`relationship`中,增加一个`single_parent=True`的参数。
|
||||
4. `merge`:默认选项。当在使用`session.merge`,合并一个对象的时候,会将使用了`relationship`相关联的对象也进行`merge`操作。
|
||||
5. `expunge`:移除操作的时候,会将相关联的对象也进行移除。这个操作只是从session中移除,并不会真正的从数据库中删除。
|
||||
6. `all`:是对`save-update, merge, refresh-expire, expunge, delete`几种的缩写。
|
||||
|
||||
## 6. 查询高级
|
||||
|
||||
### 6.1 排序:
|
||||
|
||||
1. `order_by`:可以指定根据这个表中的某个字段进行排序,如果在前面加了一个`-`,代表的是降序排序。
|
||||
|
||||
2. 在模型定义的时候指定默认排序:有些时候,不想每次在查询的时候都指定排序的方式,可以在定义模型的时候就指定排序的方式。有以下两种方式:
|
||||
|
||||
- relationship的order_by参数:在指定`relationship`的时候,传递`order_by`参数来指定排序的字段。
|
||||
|
||||
- 在模型定义中,添加以下代码:
|
||||
|
||||
```
|
||||
__mapper_args__ = {
|
||||
"order_by": title
|
||||
}
|
||||
```
|
||||
|
||||
即可让文章使用标题来进行排序。
|
||||
|
||||
3. 正向排序和反向排序:默认情况是从小到大,从前到后排序的,如果想要反向排序,可以调用排序的字段的`desc`方法。
|
||||
|
||||
### 6.2 limit、offset和切片:
|
||||
|
||||
1. `limit`:可以限制每次查询的时候只查询几条数据。
|
||||
2. `offset`:可以限制查找数据的时候过滤掉前面多少条。
|
||||
3. 切片:可以对`Query`对象使用切片操作,来获取想要的数据。
|
||||
|
||||
### 6.3 懒加载:
|
||||
|
||||
在一对多,或者多对多的时候,如果想要获取多的这一部分的数据的时候,往往能通过一个属性就可以全部获取了。比如有一个作者,想要或者这个作者的所有文章,那么可以通过`user.articles`就可以获取所有的。但有时候我们不想获取所有的数据,比如只想获取这个作者今天发表的文章,那么这时候我们可以给`relationship`传递一个`lazy='dynamic'`,以后通过`user.articles`获取到的就不是一个列表,而是一个`AppendQuery`对象了。这样就可以对这个对象再进行一层过滤和排序等操作。
|
||||
|
||||
### 6.4 查询高级:
|
||||
|
||||
1. group_by:
|
||||
|
||||
根据某个字段进行分组。比如想要根据性别进行分组,来统计每个分组分别有多少人,那么可以使用以下代码来完成:
|
||||
|
||||
```python
|
||||
session.query(User.gender,func.count(User.id)).group_by(User.gender).all()
|
||||
```
|
||||
|
||||
2. having:
|
||||
|
||||
`having`是对查找结果进一步过滤。比如只想要看未成年人的数量,那么可以首先对年龄进行分组统计人数,然后再对分组进行`having`过滤。示例代码如下:
|
||||
|
||||
```python
|
||||
result = session.query(User.age,func.count(User.id)).group_by(User.age).having(User.age >= 18).all()
|
||||
```
|
||||
|
||||
3. join方法:
|
||||
|
||||
`join`查询分为两种,一种是`inner join`,另一种是`outer join`。默认的是`inner join`,如果指定`left join`或者是`right join`则为`outer join`。如果想要查询`User`及其对应的`Address`,则可以通过以下方式来实现:
|
||||
|
||||
```python
|
||||
for u,a in session.query(User,Address).filter(User.id==Address.user_id).all():
|
||||
print(u)
|
||||
print(a)
|
||||
# 输出结果:
|
||||
> <User (id=1,name='ed',fullname='Ed Jason',password='123456')>
|
||||
> <Address id=4,email=ed@google.com,user_id=1>
|
||||
```
|
||||
|
||||
这是通过普通方式的实现,也可以通过`join`的方式实现,更加简单:
|
||||
|
||||
```python
|
||||
for u,a in session.query(User,Address).join(Address).all():
|
||||
print(u)
|
||||
print(a)
|
||||
# 输出结果:
|
||||
> <User (id=1,name='ed',fullname='Ed Jason',password='123456')>
|
||||
> <Address id=4,email=ed@google.com,user_id=1>
|
||||
```
|
||||
|
||||
当然,如果采用`outerjoin`,可以获取所有`user`,而不用在乎这个`user`是否有`address`对象,并且`outerjoin`默认为左外查询:
|
||||
|
||||
```python
|
||||
for instance in session.query(User,Address).outerjoin(Address).all():
|
||||
print(instance)
|
||||
|
||||
# 输出结果:
|
||||
(<User (id=1,name='ed',fullname='Ed Jason',password='123456')>, <Address id=4,email=ed@google.com,user_id=1>)
|
||||
(<User (id=2,name='xt',fullname='xiaotuo',password='123')>, None)
|
||||
```
|
||||
|
||||
4. 别名:
|
||||
|
||||
当多表查询的时候,有时候同一个表要用到多次,这时候用别名就可以方便的解决命名冲突的问题了:
|
||||
|
||||
```python
|
||||
from sqlalchemy.orm import aliased
|
||||
adalias1 = aliased(Address)
|
||||
adalias2 = aliased(Address)
|
||||
for username,email1,email2 in session.query(User.name,adalias1.email_address,adalias2.email_address).join(adalias1).join(adalias2).all():
|
||||
print(username,email1,email2)
|
||||
```
|
||||
|
||||
5. 子查询:
|
||||
|
||||
`sqlalchemy`也支持子查询,比如现在要查找一个用户的用户名以及该用户的邮箱地址数量。要满足这个需求,可以在子查询中找到所有用户的邮箱数(通过group by合并同一用户),然后再将结果放在父查询中进行使用:
|
||||
|
||||
```python
|
||||
from sqlalchemy.sql import func
|
||||
# 构造子查询
|
||||
stmt = session.query(Address.user_id.label('user_id'),func.count(*).label('address_count')).group_by(Address.user_id).subquery()
|
||||
|
||||
# 将子查询放到父查询中
|
||||
for u,count in session.query(User,stmt.c.address_count).outerjoin(stmt,User.id==stmt.c.user_id).order_by(User.id):
|
||||
print(u,count)
|
||||
```
|
||||
|
||||
从上面我们可以看到,一个查询如果想要变为子查询,则是通过`subquery()`方法实现,变成子查询后,通过`子查询.c`属性来访问查询出来的列。以上方式只能查询某个对象的具体字段,如果要查找整个实体,则需要通过`aliased`方法,示例如下:
|
||||
|
||||
```python
|
||||
stmt = session.query(Address)
|
||||
adalias = aliased(Address,stmt)
|
||||
for user,address in session.query(User,stmt).join(stmt,User.addresses):
|
||||
print(user,address)
|
||||
```
|
||||
|
||||
## 7. Flask-SQLAlchemy库
|
||||
|
||||
另外一个库,叫做`Flask-SQLAlchemy`,`Flask-SQLAlchemy`是对`SQLAlchemy`进行了一个简单的封装,使得我们在`flask`中使用`sqlalchemy`更加的简单。可以通过`pip install flask-sqlalchemy`。使用`Flask-SQLAlchemy`的流程如下:
|
||||
|
||||
1. 数据库初始化:数据库初始化不再是通过`create_engine`,请看以下示例:
|
||||
|
||||
```python
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from constants import DB_URI
|
||||
app = Flask(__name__)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = DB_URI
|
||||
db = SQLAlchemy(app)
|
||||
```
|
||||
|
||||
2. `ORM`类:之前都是通过`Base = declarative_base()`来初始化一个基类,然后再继承,在`Flask-SQLAlchemy`中更加简单了(代码依赖以上示例):
|
||||
|
||||
```python
|
||||
class User(db.Model):
|
||||
id = db.Column(db.Integer,primary_key=True)
|
||||
username = db.Column(db.String(80),unique=True)
|
||||
email = db.Column(db.String(120),unique=True)
|
||||
def __init__(self,username,email):
|
||||
self.username = username
|
||||
self.email = email
|
||||
def __repr__(self):
|
||||
return '<User %s>' % self.username
|
||||
```
|
||||
|
||||
3. 映射模型到数据库表:使用`Flask-SQLAlchemy`所有的类都是继承自`db.Model`,并且所有的`Column`和数据类型也都成为`db`的一个属性。但是有个好处是不用写表名了,`Flask-SQLAlchemy`会自动将类名小写化,然后映射成表名。
|
||||
写完类模型后,要将模型映射到数据库的表中,使用以下代码创建所有的表:
|
||||
|
||||
```python
|
||||
db.create_all()
|
||||
```
|
||||
|
||||
4. 添加数据:这时候就可以在数据库中看到已经生成了一个`user`表了。接下来添加数据到表中:
|
||||
|
||||
```python
|
||||
admin = User('admin','admin@example.com')
|
||||
guest = User('guest','guest@example.com')
|
||||
db.session.add(admin)
|
||||
db.session.add(guest)
|
||||
db.session.commit()
|
||||
```
|
||||
|
||||
添加数据和之前的没有区别,只是`session`成为了一个`db`的属性。
|
||||
|
||||
5. 查询数据:查询数据不再是之前的`session.query`了,而是将`query`属性放在了`db.Model`上,所以查询就是通过`Model.query`的方式进行查询了:
|
||||
|
||||
```python
|
||||
users = User.query.all()
|
||||
# 再如:
|
||||
admin = User.query.filter_by(username='admin').first()
|
||||
# 或者:
|
||||
admin = User.query.filter(User.username='admin').first()
|
||||
```
|
||||
|
||||
6. 删除数据:删除数据跟添加数据类似,只不过`session`是`db`的一个属性而已:
|
||||
|
||||
```python
|
||||
db.session.delete(admin)
|
||||
db.session.commit()
|
||||
```
|
||||
|
||||
## 8. MySQL8图文教程
|
||||
|
||||
### 8.1 下载:
|
||||
|
||||
在以下链接中下载`MySQL8 Community`:https://dev.mysql.com/downloads/windows/installer/8.0.html
|
||||

|
||||
|
||||
### 8.2 安装:
|
||||
|
||||
下载完成后,就可以双击`msi`文件进行安装了。安装步骤如下。
|
||||
|
||||
1. 第一步:选择`Developer Default`:
|
||||

|
||||
2. 点击Execute:
|
||||

|
||||
然后再点击`Next`执行下一步
|
||||

|
||||
3. 点击Next:
|
||||

|
||||
4. 保持默认值,点击Next:
|
||||

|
||||
5. 设置密码加密方式,用默认的即可,然后点击Next:
|
||||

|
||||
6. 设置密码,两次密码输入保持一致,以后登录`MySQL`就要用这个密码了,所以要记住:
|
||||

|
||||
7. 保持默认值,点击Next:
|
||||

|
||||
8. 点击Next:
|
||||

|
||||
执行完后点击Finish
|
||||

|
||||
9. 重新点击Next:
|
||||

|
||||
10. 点击Next:
|
||||

|
||||
11. 输入密码,点击Check:
|
||||

|
||||
12. 点击Finish,完成安装:
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
BIN
04.Flask/04.SQLAlchemy/2wrbfuJzg3RVPL8UaLnw8U.png
Normal file
After Width: | Height: | Size: 138 KiB |
BIN
04.Flask/04.SQLAlchemy/6jGy66928WdhmKVvX6PtgY.png
Normal file
After Width: | Height: | Size: 97 KiB |
BIN
04.Flask/04.SQLAlchemy/8P5sFSmXzjYHNiN76MStNS.png
Normal file
After Width: | Height: | Size: 169 KiB |
BIN
04.Flask/04.SQLAlchemy/JeVsr6NJbo73HJhN2qEC5o.png
Normal file
After Width: | Height: | Size: 106 KiB |
BIN
04.Flask/04.SQLAlchemy/JkerUJzxSe9gkEZM8kq4td.png
Normal file
After Width: | Height: | Size: 138 KiB |
BIN
04.Flask/04.SQLAlchemy/MW2uxnkekaQG5SiWFcLcfX.png
Normal file
After Width: | Height: | Size: 101 KiB |
BIN
04.Flask/04.SQLAlchemy/PYHwMTRXxKjfBKyYG6b2Vi.png
Normal file
After Width: | Height: | Size: 166 KiB |
BIN
04.Flask/04.SQLAlchemy/TCiEyKKyv5b9iJRT48HbMY.png
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
04.Flask/04.SQLAlchemy/XYQtENVVzPvVVARq7Qt74D.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
04.Flask/04.SQLAlchemy/Yfgrhaxrx6NCj9gSHFbHuV.png
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
04.Flask/04.SQLAlchemy/ZhBkRdPgvRrnfAQrseTfs3.png
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
04.Flask/04.SQLAlchemy/axbEskKZYY2XaaKfPgrcnD.png
Normal file
After Width: | Height: | Size: 209 KiB |
BIN
04.Flask/04.SQLAlchemy/eyw6PKK6tH8uAXPtd9AwiD.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
04.Flask/04.SQLAlchemy/hT2fsYR8NemfdcVhUpjYSQ.png
Normal file
After Width: | Height: | Size: 157 KiB |
BIN
04.Flask/04.SQLAlchemy/mZEdUQoEPJuyXBtZbWsyRF.png
Normal file
After Width: | Height: | Size: 107 KiB |
BIN
04.Flask/04.SQLAlchemy/p2YQ6oveDpNYrYQsnejFyV.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
04.Flask/04.SQLAlchemy/rHS8Ny8FHDrwhvbUWv4XAC.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
04.Flask/04.SQLAlchemy/wYbVmHwu4Crndi4mMMjH9Z.png
Normal file
After Width: | Height: | Size: 142 KiB |
BIN
04.Flask/04.SQLAlchemy/wkgYVxahsubMtRv4NrMk9h.png
Normal file
After Width: | Height: | Size: 146 KiB |
284
04.Flask/05.ORM迁移.md
Normal file
@@ -0,0 +1,284 @@
|
||||
# ORM迁移
|
||||
|
||||
## 1. alembic
|
||||
|
||||
`alembic`是`sqlalchemy`的作者开发的。用来做`OMR`模型与数据库的迁移与映射。`alembic`使用方式跟`git`有点了类似,表现在两个方面。
|
||||
|
||||
1. 第一个,`alembic`的所有命令都是以`alembic`开头
|
||||
2. 第二,`alembic`的迁移文件也是通过版本进行控制的。首先,通过`pip install alembic`进行安装。
|
||||
|
||||
以下将解释`alembic`的用法。
|
||||
|
||||
### 1.1 操作步骤
|
||||
|
||||
#### 1.1.1 初始化仓库:
|
||||
|
||||
初始化`alembic`仓库。在终端中,`cd`到你的项目目录中,然后执行命令`alembic init alembic`,创建一个名叫`alembic`的仓库。
|
||||
|
||||
#### 1.1.2 创建模型(ORM)类:
|
||||
|
||||
比如这里创建一个`models.py`模块,然后在里面定义你的模型类,示例代码如下:
|
||||
|
||||
```python
|
||||
from sqlalchemy import Column,Integer,String,create_engine,Text
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
Base = declarative_base()
|
||||
class User(Base):
|
||||
__tablename__ = 'user'
|
||||
|
||||
id = Column(Integer,primary_key=True)
|
||||
username = Column(String(20),nullable=False)
|
||||
password = Column(String(100),nullable=False)
|
||||
|
||||
class Article(Base):
|
||||
__tablename__ = 'article'
|
||||
|
||||
id = Column(Integer,primary_key=True)
|
||||
title = Column(String(100),nullable=False)
|
||||
content = Column(Text, nullable=False)
|
||||
```
|
||||
|
||||
#### 1.1.3 修改配置文件:
|
||||
|
||||
- 在`alembic.ini`中设置数据库的连接,`sqlalchemy.url = driver://user:pass@localhost/dbname`,比如以`mysql`数据库为例,则配置后的代码为:
|
||||
|
||||
```python
|
||||
sqlalchemy.url = mysql+mysqldb://root:root@localhost/alembic_demo?charset=utf8
|
||||
```
|
||||
|
||||
- 为了使用模型类更新数据库,需要在`env.py`文件中设置`target_metadata`,默认为`target_metadata=None`。使用`sys`模块把当前项目的路径导入到`path`中:
|
||||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/../")
|
||||
from models import Base
|
||||
... #省略代码
|
||||
target_metadata = Base.metadata # 设置创建模型的元类
|
||||
... #省略代码
|
||||
```
|
||||
|
||||
#### 1.1.4 生成迁移文件
|
||||
|
||||
使用命令`alembic revision --autogenerate -m "message"`可以将当前模型中的状态生成迁移文件。
|
||||
|
||||
#### 1.1.5 更新数据库
|
||||
|
||||
使用`alembic upgrade head`将刚刚生成的迁移文件,真正映射到数据库中。同理,如果要降级,那么使用`alembic downgrade head`。
|
||||
|
||||
#### 1.1.6 重复
|
||||
|
||||
如果以后修改了代码,则重复4~5的步骤。
|
||||
|
||||
### 1.2 命令和参数解释:
|
||||
|
||||
- init:创建一个`alembic`仓库。
|
||||
- revision:创建一个新的版本文件。
|
||||
- –autogenerate:自动将当前模型的修改,生成迁移脚本。
|
||||
- -m:本次迁移做了哪些修改,用户可以指定这个参数,方便回顾。
|
||||
- upgrade:将指定版本的迁移文件映射到数据库中,会执行版本文件中的`upgrade`函数。如果有多个迁移脚本没有被映射到数据库中,那么会执行多个迁移脚本。
|
||||
- [head]:代表最新的迁移脚本的版本号。
|
||||
- downgrade:会执行指定版本的迁移文件中的`downgrade`函数。
|
||||
- heads:展示head指向的脚本文件版本号。
|
||||
- history:列出所有的迁移版本及其信息。
|
||||
- current:展示当前数据库中的版本号。
|
||||
|
||||
另外,在你第一次执行`upgrade`的时候,就会在数据库中创建一个名叫`alembic_version`表,这个表只会有一条数据,记录当前数据库映射的是哪个版本的迁移文件。
|
||||
|
||||
### 1.3 经典错误:
|
||||
|
||||
| 错误描述 | 原因 | 解决办法 |
|
||||
| :----------------------------------------------------------- | :----------------------------------------------------------- | :----------------------------------------------------------- |
|
||||
| `FAILED: Target database is not up to date.` | 主要是`heads`和`current`不相同。`current`落后于`heads`的版本。 | 将current移动到head上。alembic upgrade head |
|
||||
| `FAILED: Can't locate revision identified by '77525ee61b5b'` | 数据库中存的版本号不在迁移脚本文件中 | 删除数据库的`alembic_version`表中的数据,重新执行`alembic upgrade head` |
|
||||
|
||||
## 2. Flask-Migrate插件
|
||||
|
||||
在实际的开发环境中,经常会发生数据库修改的行为。一般我们修改数据库不会直接手动的去修改,而是去修改`ORM`对应的模型,然后再把模型映射到数据库中。这时候如果有一个工具能专门做这种事情,就显得非常有用了,而`flask-migrate`就是做这个事情的。`flask-migrate`是基于`Alembic`进行的一个封装,并集成到`Flask`中,而所有的迁移操作其实都是`Alembic`做的,他能跟踪模型的变化,并将变化映射到数据库中。
|
||||
|
||||
使用`Flask-Migrate`需要安装,命令如下:
|
||||
|
||||
```
|
||||
pip install flask-migrate
|
||||
```
|
||||
|
||||
### 2.1 基本用法
|
||||
|
||||
要让`Flask-Migrate`能够管理`app`中的数据库,需要使用`Migrate(app,db)`来绑定`app`和数据库。假如现在有以下`app`文件:
|
||||
|
||||
```python
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from constants import DB_URI
|
||||
from flask_migrate import Migrate
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = DB_URI
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
|
||||
db = SQLAlchemy(app)
|
||||
## 绑定app和数据库
|
||||
migrate = Migrate(app,db)
|
||||
|
||||
class User(db.Model):
|
||||
id = db.Column(db.Integer,primary_key=True)
|
||||
username = db.Column(db.String(20))
|
||||
|
||||
addresses = db.relationship('Address',backref='user')
|
||||
|
||||
class Address(db.Model):
|
||||
id = db.Column(db.Integer,primary_key=True)
|
||||
email_address = db.Column(db.String(50))
|
||||
user_id = db.Column(db.Integer,db.ForeignKey('user.id'))
|
||||
|
||||
db.create_all()
|
||||
|
||||
@app.route('/')
|
||||
def hello_world():
|
||||
return 'Hello World!'
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
```
|
||||
|
||||
之后,就可以在命令行中映射`ORM`了。首先需要初始化一个迁移文件夹:
|
||||
|
||||
```python
|
||||
flask db init
|
||||
```
|
||||
|
||||
然后再把当前的模型添加到迁移文件中:
|
||||
|
||||
```python
|
||||
flask db migrate
|
||||
```
|
||||
|
||||
最后再把迁移文件中对应的数据库操作,真正的映射到数据库中:
|
||||
|
||||
```python
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
### 2.2 项目重构
|
||||
|
||||
现在是所有代码都写在一个文件中,这样会导致文件会越来越乱。所以进行一下项目重构,设置为以下的目录结构:
|
||||

|
||||
以下对各个文件的作用进行解释。
|
||||
|
||||
#### 2.2.1 constants.py文件
|
||||
|
||||
常量文件,用来存放数据库配置。
|
||||
|
||||
```python
|
||||
## constants.py
|
||||
HOSTNAME = '127.0.0.1'
|
||||
PORT = '3306'
|
||||
DATABASE = 'xt_flask_migrate'
|
||||
USERNAME = 'root'
|
||||
PASSWORD = 'root'
|
||||
DB_URI = 'mysql+mysqldb://{}:{}@{}:{}/{}'.format(USERNAME,PASSWORD,HOSTNAME,PORT,DATABASE)
|
||||
```
|
||||
|
||||
#### 2.2.2 ext.py文件
|
||||
|
||||
把`db`变量放到一个单独的文件,而不是放在主`app`文件。这样做的目的是为了在大型项目中如果`db`被多个模型文件引用的话,会造成`from your_app import db`这样的方式,但是往往也在`your_app.py`中也会引入模型文件定义的类,这就造成了循环引用。所以最好的办法是把它放在不依赖其他模块的独立文件中。
|
||||
|
||||
```python
|
||||
## ext.py
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
db = SQLAlchemy()
|
||||
```
|
||||
|
||||
#### 2.2.3 models.py文件
|
||||
|
||||
模型文件,用来存放所有的模型,并且注意,因为这里使用的是`flask-script`的方式进行模型和表的映射,因此不需要使用`db.create_all()`的方式创建数据库。
|
||||
|
||||
```python
|
||||
## models.py
|
||||
from ext import db
|
||||
class User(db.Model):
|
||||
id = db.Column(db.Integer,primary_key=True)
|
||||
username = db.Column(db.String(50))
|
||||
addresses = db.relationship('Address',backref='user')
|
||||
|
||||
def __init__(self,username):
|
||||
self.username = username
|
||||
|
||||
class Address(db.Model):
|
||||
id = db.Column(db.Integer,primary_key=True)
|
||||
email_address = db.Column(db.String(50))
|
||||
user_id = db.Column(db.Integer,db.ForeignKey('user.id'))
|
||||
|
||||
def __init__(self,email_address):
|
||||
self.email_address = email_address
|
||||
```
|
||||
|
||||
#### 2.2.4 app.py文件
|
||||
|
||||
这个是主`app`文件,运行文件。并且因为`db`被放到另外一个文件中,所以使用`db.init_app(app)`的方式来绑定数据库。
|
||||
|
||||
```python
|
||||
## app.py
|
||||
from flask import Flask
|
||||
from ext import db
|
||||
|
||||
app = Flask(__name__)
|
||||
db.init_app(app)
|
||||
|
||||
@app.route('/')
|
||||
def hello_world():
|
||||
return 'Hello World!'
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
```
|
||||
|
||||
### 2.3 迁移命令:
|
||||
|
||||
通过以上项目重构后,后续我们就只要通过以下三个命令即可完成迁移操作了。
|
||||
|
||||
1. 运行命令来初始化迁移文件:
|
||||
|
||||
```python
|
||||
python manage.py db init
|
||||
```
|
||||
|
||||
2. 运行命令来将模型的映射添加到文件中:
|
||||
|
||||
```python
|
||||
python manage.py db migrate
|
||||
```
|
||||
|
||||
3. 添加将映射文件真正的映射到数据库中:
|
||||
|
||||
```python
|
||||
python manage.py db upgrade
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
BIN
04.Flask/05.ORM迁移/rKAhcte9Fcf6DXAeQATfaV.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
319
04.Flask/06.Flask-WTF插件.md
Normal file
@@ -0,0 +1,319 @@
|
||||
# Flask-WTF插件
|
||||
|
||||
## 1. Flask-WTF表单验证
|
||||
|
||||
`Flask-WTF`是简化了`WTForms`操作的一个第三方库。`WTForms`表单的两个主要功能是验证用户提交数据的合法性以及渲染模板。当然还包括一些其他的功能:`CSRF保护`,文件上传等。安装`Flask-WTF`默认也会安装`WTForms`,因此使用以下命令来安装`Flask-WTF`:
|
||||
|
||||
```
|
||||
pip install flask-wtf
|
||||
```
|
||||
|
||||
### 1.1 表单验证:
|
||||
|
||||
安装完`Flask-WTF`后。来看下第一个功能,就是用表单来做数据验证,现在有一个`forms.py`文件,然后在里面创建一个`RegistForm`的注册验证表单:
|
||||
|
||||
```python
|
||||
class RegistForm(Form):
|
||||
name = StringField(validators=[length(min=4,max=25)])
|
||||
email = StringField(validators=[email()])
|
||||
password = StringField(validators=[DataRequired(),length(min=6,max=10),EqualTo('confirm')])
|
||||
confirm = StringField()
|
||||
```
|
||||
|
||||
在这个里面指定了需要上传的参数,并且指定了验证器,比如`name`的长度应该在`4-25`之间。`email`必须要满足邮箱的格式。`password`长度必须在`6-10`之间,并且应该和`confirm`相等才能通过验证。
|
||||
|
||||
写完表单后,接下来就是`regist.html`文件:
|
||||
|
||||
```python
|
||||
<form action="/regist/" method="POST">
|
||||
<table>
|
||||
<tr>
|
||||
<td>用户名:</td>
|
||||
<td><input type="text" name="name"></td>
|
||||
</tr>
|
||||
<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="password" name="confirm"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><input type="submit" value="提交"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
```
|
||||
|
||||
再来看视图函数`regist`:
|
||||
|
||||
```python
|
||||
@app.route('/regist/',methods=['POST','GET'])
|
||||
def regist():
|
||||
form = RegistForm(request.form)
|
||||
if request.method == 'POST' and form.validate():
|
||||
user = User(name=form.name.data,email=form.email.data,password=form.password.data)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return u'注册成功!'
|
||||
return render_template('regist.html')
|
||||
```
|
||||
|
||||
`RegistForm`传递的是`request.form`进去进行初始化,并且判断`form.validate`会返回用户提交的数据是否满足表单的验证。
|
||||
|
||||
### 1.2 渲染模板:
|
||||
|
||||
`form`还可以渲染模板,让你少写了一丢丢的代码,比如重写以上例子,`RegistForm`表单代码如下:
|
||||
|
||||
```python
|
||||
class RegistForm(Form):
|
||||
name = StringField(u'用户名:',validators=[length(min=4,max=25)])
|
||||
email = StringField(u'邮箱:'validators=[email()])
|
||||
password = StringField(u'密码:',validators=[DataRequired(),length(min=6,max=10),EqualTo('confirm')])
|
||||
confirm = StringField(u'确认密码:')
|
||||
```
|
||||
|
||||
以上增加了第一个位置参数,用来在html文件中,做标签提示作用。
|
||||
|
||||
在`app`中的视图函数中,修改为如下:
|
||||
|
||||
```python
|
||||
@app.route('/regist/',methods=['POST','GET'])
|
||||
def regist():
|
||||
form = RegistForm(request.form)
|
||||
if request.method == 'POST' and form.validate():
|
||||
user = User(name=form.name.data,email=form.email.data,password=form.password.data)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return u'注册成功!'
|
||||
return render_template('regist.html',form=form)
|
||||
```
|
||||
|
||||
以上唯一的不同是在渲染模板的时候传入了`form`表单参数进去,这样在模板中就可以使用表单`form`变量了。
|
||||
|
||||
接下来看下`regist.html`文件:
|
||||
|
||||
```python
|
||||
<form action="/regist/" method="POST">
|
||||
<table>
|
||||
<tr>
|
||||
<td>{{ form.name.label }}</td>
|
||||
<td>{{ form.name() }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ form.email.label }}</td>
|
||||
<td>{{ form.email() }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ form.password.label }}</td>
|
||||
<td>{{ form.password() }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ form.confirm.label }}</td>
|
||||
<td>{{ form.confirm() }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><input type="submit" value="提交"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
```
|
||||
|
||||
## 2. Flask-WTF常用字段和验证器
|
||||
|
||||
### 2.1 Field常用参数:
|
||||
|
||||
在使用`Field`的时候,经常需要传递一些参数进去,以下将对一些常用的参数进行解释:
|
||||
|
||||
- label(第一个参数):`Field`的label的文本。
|
||||
- validators:验证器。
|
||||
- id:`Field`的id属性,默认不写为该属性名。
|
||||
- default:默认值。
|
||||
- widget:指定的`html`控件。
|
||||
|
||||
### 2.2 常用Field:
|
||||
|
||||
- BooleanField:布尔类型的Field,渲染出去是`checkbox`。
|
||||
|
||||
- FileField:文件上传Field。
|
||||
|
||||
```python
|
||||
# forms.py
|
||||
from flask_wtf.file import FileField,FileAllowed,FileRequired
|
||||
class UploadForm(FlaskForm):
|
||||
avatar = FileField(u'头像:',validators=[FileRequired(),FileAllowed([])])
|
||||
|
||||
# app.py
|
||||
@app.route('/profile/',methods=('POST','GET'))
|
||||
def profile():
|
||||
form = ProfileForm()
|
||||
if form.validate_on_submit():
|
||||
filename = secure_filename(form.avatar.data.filename)
|
||||
form.avatar.data.save(os.path.join(app.config['UPLOAD_FOLDER'],filename))
|
||||
return u'上传成功'
|
||||
|
||||
return render_template('profile.html',form=form)
|
||||
```
|
||||
|
||||
- FloatField:浮点数类型的Field,但是渲染出去的时候是`text`的input。
|
||||
|
||||
- IntegerField:整形的Field。同FloatField。
|
||||
|
||||
- RadioField:`radio`类型的`input`。表单例子如下:
|
||||
|
||||
```python
|
||||
# form.py
|
||||
class RegistrationForm(FlaskForm):
|
||||
gender = wtforms.RadioField(u'性别:',validators=[DataRequired()])
|
||||
```
|
||||
|
||||
模板文件代码如下:
|
||||
|
||||
```python
|
||||
<tr>
|
||||
<td>
|
||||
{{ form.gender.label }}
|
||||
</td>
|
||||
<td>
|
||||
{% for gender in form.gender %}
|
||||
{{ gender.label }}
|
||||
{{ gender }}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
```
|
||||
|
||||
`app.py`文件的代码如下,给`gender`添加了`choices`:
|
||||
|
||||
```python
|
||||
@app.route('/register/',methods=['POST','GET'])
|
||||
def register():
|
||||
form = RegistrationForm()
|
||||
form.gender.choices = [('1',u'男'),('2',u'女')]
|
||||
if form.validate_on_submit():
|
||||
return u'success'
|
||||
|
||||
return render_template('register.html',form=form)
|
||||
```
|
||||
|
||||
- SelectField:类似于`RadioField`。看以下示例:
|
||||
|
||||
```python
|
||||
# forms.py
|
||||
class ProfileForm(FlaskForm):
|
||||
language = wtforms.SelectField('Programming Language',choices=[('cpp','C++'),('py','python'),('text','Plain Text')],validators=[DataRequired()])
|
||||
```
|
||||
|
||||
再来看`app.py`文件:
|
||||
|
||||
```python
|
||||
@app.route('/profile/',methods=('POST','GET'))
|
||||
def profile():
|
||||
form = ProfileForm()
|
||||
if form.validate_on_submit():
|
||||
print form.language.data
|
||||
return u'上传成功'
|
||||
return render_template('profile.html',form=form)
|
||||
```
|
||||
|
||||
模板文件为:
|
||||
|
||||
```html
|
||||
<form action="/profile/" method="POST">
|
||||
{{ form.csrf_token }}
|
||||
{{ form.language.label }}
|
||||
{{ form.language() }}
|
||||
<input type="submit">
|
||||
</form>
|
||||
```
|
||||
|
||||
- StringField:渲染到模板中的类型为`<input type='text'>`,并且是最基本的文本验证。
|
||||
|
||||
- PasswordField:渲染出来的是一个`password`的`input`标签。
|
||||
|
||||
- TextAreaField:渲染出来的是一个`textarea`。
|
||||
|
||||
### 2.3 常用的验证器:
|
||||
|
||||
数据发送过来,经过表单验证,因此需要验证器来进行验证,以下对一些常用的内置验证器进行讲解:
|
||||
|
||||
- Email:验证上传的数据是否为邮箱。
|
||||
- EqualTo:验证上传的数据是否和另外一个字段相等,常用的就是密码和确认密码两个字段是否相等。
|
||||
- InputRequired:原始数据的需要验证。如果不是特殊情况,应该使用`InputRequired`。
|
||||
- Length:长度限制,有min和max两个值进行限制。
|
||||
- NumberRange:数字的区间,有min和max两个值限制,如果处在这两个数字之间则满足。
|
||||
- Regexp:自定义正则表达式。
|
||||
- URL:必须要是`URL`的形式。
|
||||
- UUID:验证`UUID`。
|
||||
|
||||
### 2.4 自定义验证字段:
|
||||
|
||||
使用`validate_fieldname(self,field)`可以对某个字段进行更加详细的验证,如下:
|
||||
|
||||
```python
|
||||
class ProfileForm(FlaskForm):
|
||||
name = wtforms.StringField('name',[validators.InputRequired()])
|
||||
def validate_name(self,field):
|
||||
if len(field.data) > 5:
|
||||
raise wtforms.ValidationError(u'超过5个字符')
|
||||
```
|
||||
|
||||
## 3. CSRF保护:
|
||||
|
||||
在flask的表单中,默认是开启了`csrf`保护功能的,如果你想关闭表单的`csrf`保护,可以在初始化表单的时候传递`csrf_enabled=False`进去来关闭`csrf`保护。如果你想关闭这种默认的行为。如果你想在没有表单存在的请求视图函数中也添加`csrf`保护,可以开启全局的`csrf`保护功能:
|
||||
|
||||
```python
|
||||
csrf = CsrfProtect()
|
||||
csrf.init_app(app)
|
||||
```
|
||||
|
||||
或者是针对某一个视图函数,使用`csrf.protect`装饰器来开启`csrf`保护功能。并且如果已经开启了全局的`csrf`保护,想要关闭某个视图函数的`csrf`保护功能,可以使用`csrf.exempt`装饰器来取消本视图函数的保护功能。
|
||||
|
||||
### 3.1 AJAX的CSRF保护:
|
||||
|
||||
在`AJAX`中要使用`csrf`保护,则必须手动的添加`X-CSRFToken`到`Header`中。但是`CSRF`从哪里来,还是需要通过模板给渲染,而`Flask`比较推荐的方式是在`meta`标签中渲染`csrf`,如下:
|
||||
|
||||
```html
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
```
|
||||
|
||||
如果要发送`AJAX`请求,则在发送之前要添加`CSRF`,代码如下(使用了jQuery):
|
||||
|
||||
```javascript
|
||||
var csrftoken = $('meta[name=csrf-token]').attr('content')
|
||||
$.ajaxSetup({
|
||||
beforeSend: function(xhr, settings) {
|
||||
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
|
||||
xhr.setRequestHeader("X-CSRFToken", csrftoken)
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
174
04.Flask/07.上下文与信号.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# 上下文与信号
|
||||
|
||||
## 1. Flask上下文
|
||||
|
||||
`Flask`项目中有两个上下文,一个是应用上下文(app),另外一个是请求上下文(request)。请求上下文`request`和应用上下文`current_app`都是一个全局变量。所有请求都共享的。`Flask`有特殊的机制可以保证每次请求的数据都是隔离的,即A请求所产生的数据不会影响到B请求。所以可以直接导入`request`对象,也不会被一些脏数据影响了,并且不需要在每个函数中使用request的时候传入`request`对象。这两个上下文具体的实现方式和原理可以没必要详细了解。只要了解这两个上下文的四个属性就可以了:
|
||||
|
||||
- `request`:请求上下文上的对象。这个对象一般用来保存一些请求的变量。比如`method`、`args`、`form`等。
|
||||
- `session`:请求上下文上的对象。这个对象一般用来保存一些会话信息。
|
||||
- `current\_app`:返回当前的app。
|
||||
- `g`:应用上下文上的对象。处理请求时用作临时存储的对象。
|
||||
|
||||
### 1.1 常用的钩子函数
|
||||
|
||||
- before_first_request:处理第一次请求之前执行。例如以下代码:
|
||||
|
||||
```python
|
||||
@app.before_first_request
|
||||
def first_request():
|
||||
print 'first time request'
|
||||
```
|
||||
|
||||
- before_request:在每次请求之前执行。通常可以用这个装饰器来给视图函数增加一些变量。例如以下代码:
|
||||
|
||||
```python
|
||||
@app.before_request
|
||||
def before_request():
|
||||
if not hasattr(g,'user'):
|
||||
setattr(g,'user','xxxx')
|
||||
```
|
||||
|
||||
- teardown_appcontext:不管是否有异常,注册的函数都会在每次请求之后执行。
|
||||
|
||||
```python
|
||||
@app.teardown_appcontext
|
||||
def teardown(exc=None):
|
||||
if exc is None:
|
||||
db.session.commit()
|
||||
else:
|
||||
db.session.rollback()
|
||||
db.session.remove()
|
||||
```
|
||||
|
||||
- template_filter:在使用`Jinja2` 模板的时候自定义过滤器。比如可以增加一个`upper` 的过滤器(当然Jinja2已经存在这个过滤器,本示例只是为了演示作用):
|
||||
|
||||
```python
|
||||
@app.template_filter
|
||||
def upper_filter(s):
|
||||
return s.upper()
|
||||
```
|
||||
|
||||
- context_processor:上下文处理器。返回的字典中的键可以在模板上下文中使用。例如:
|
||||
|
||||
```python
|
||||
@app.context_processor
|
||||
return {'current_user':'xxx'}
|
||||
```
|
||||
|
||||
- errorhandler:errorhandler接收状态码,可以自定义返回这种状态码的响应的处理方法。例如:
|
||||
|
||||
```python
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(error):
|
||||
return 'This page does not exist',404
|
||||
```
|
||||
|
||||
## 2. flask信号:
|
||||
|
||||
### 2.1 安装:
|
||||
|
||||
`flask`中的信号使用的是一个第三方插件,叫做`blinker`。通过`pip list`看一下,如果没有安装,通过以下命令即可安装`blinker`:
|
||||
|
||||
```
|
||||
pip install blinker
|
||||
```
|
||||
|
||||
### 2.2 内置信号:
|
||||
|
||||
`flask`内置集中常用的信号:
|
||||
|
||||
1. `flask.template_rendered`:模版渲染完毕后发送,示例如下:
|
||||
|
||||
```python
|
||||
from flask import template_rendered
|
||||
def log_template_renders(sender,template,context,*args):
|
||||
print 'sender:',sender
|
||||
print 'template:',template
|
||||
print 'context:',context
|
||||
|
||||
template_rendered.connect(log_template_renders,app)
|
||||
```
|
||||
|
||||
2. `flask.request_started`:请求开始之前,在到达视图函数之前发送,订阅者可以调用`request`之类的标准全局代理访问请求。示例如下:
|
||||
|
||||
```python
|
||||
def log_request_started(sender,**extra):
|
||||
print 'sender:',sender
|
||||
print 'extra:',extra
|
||||
request_started.connect(log_request_started,app)
|
||||
```
|
||||
|
||||
3. `flask.request_finished`:请求结束时,在响应发送给客户端之前发送,可以传递`response`,示例代码如下:
|
||||
|
||||
```python
|
||||
def log_request_finished(sender,response,*args):
|
||||
print 'response:',response
|
||||
request_finished.connect(log_request_finished,app)
|
||||
```
|
||||
|
||||
4. `flask.got_request_exception`:在请求过程中抛出异常时发送,异常本身会通过`exception`传递到订阅的函数。示例代码如下:
|
||||
|
||||
```python
|
||||
def log_exception_finished(sender,exception,*args):
|
||||
print 'sender:',sender
|
||||
print type(exception)
|
||||
got_request_exception.connect(log_exception_finished,app)
|
||||
```
|
||||
|
||||
5. `flask.request_tearing_down`:请求被销毁的时候发送,即使在请求过程中发生异常,也会发送,示例代码如下:
|
||||
|
||||
```python
|
||||
def log_request_tearing_down(sender,**kwargs):
|
||||
print 'coming...'
|
||||
request_tearing_down.connect(log_request_tearing_down,app)
|
||||
```
|
||||
|
||||
6. `flask.appcontext_tearing_down`:在应用上下文销毁的时候发送,它总是会被调用,即使发生异常。示例代码如下:
|
||||
|
||||
```python
|
||||
def log_appcontext_tearing_down(sender,**kwargs):
|
||||
print 'coming...'
|
||||
appcontext_tearing_down.connect(log_appcontext_tearing_down,app)
|
||||
```
|
||||
|
||||
### 2.3 自定义信号:
|
||||
|
||||
自定义信号分为3步,第一是定义一个信号,第二是监听一个信号,第三是发送一个信号。以下将对这三步进行讲解:
|
||||
|
||||
1. 定义信号:定义信号需要使用到`blinker`这个包的`Namespace`类来创建一个命名空间。比如定义一个在访问了某个视图函数的时候的信号。示例代码如下:
|
||||
|
||||
```python
|
||||
from blinker import Namespace
|
||||
|
||||
mysignal = Namespace()
|
||||
visit_signal = mysignal.signal('visit-signal')
|
||||
```
|
||||
|
||||
2. 监听信号:监听信号使用`singal`对象的`connect`方法,在这个方法中需要传递一个函数,用来接收以后监听到这个信号该做的事情。示例代码如下:
|
||||
|
||||
```python
|
||||
def visit_func(sender,username):
|
||||
print(sender)
|
||||
print(username)
|
||||
|
||||
mysignal.connect(visit_func)
|
||||
```
|
||||
|
||||
3. 发送信号:发送信号使用`singal`对象的`send`方法,这个方法可以传递一些其他参数过去。示例代码如下:
|
||||
|
||||
```python
|
||||
mysignal.send(username='zhiliao')
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
195
04.Flask/08.Flask-Restful.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# Flask-Restful
|
||||
|
||||
## 1. Restful API规范
|
||||
|
||||
`restful api`是用于在前端与后台进行通信的一套规范。使用这个规范可以让前后端开发变得更加轻松。以下将讨论这套规范的一些设计细节。
|
||||
|
||||
### 1.1 协议:
|
||||
|
||||
采用`http`或者`https`协议。
|
||||
|
||||
### 1.2 数据传输格式:
|
||||
|
||||
数据之间传输的格式应该都使用`json`,而不使用`xml`。
|
||||
|
||||
### 1.3 url链接:
|
||||
|
||||
url链接中,不能有动词,只能有名词。并且对于一些名词,如果出现复数,那么应该在后面加`s`。
|
||||
|
||||
比如:获取文章列表,应该使用`articles`,而不应该使用`get_article`
|
||||
|
||||
### 1.4 HTTP请求的方法:
|
||||
|
||||
1. `GET`:从服务器上获取资源。
|
||||
2. `POST`:在服务器上新创建一个资源。
|
||||
3. `PUT`:在服务器上更新资源。(客户端提供所有改变后的数据)
|
||||
4. `PATCH`:在服务器上更新资源。(客户端只提供需要改变的属性)
|
||||
5. `DELETE`:从服务器上删除资源。
|
||||
|
||||
**示例如下:**
|
||||
|
||||
- `GET /users/`:获取所有用户。
|
||||
- `POST /user/`:新建一个用户。
|
||||
- `GET /user/id/`:根据id获取一个用户。
|
||||
- `PUT /user/id/`:更新某个id的用户的信息(需要提供用户的所有信息)。
|
||||
- `PATCH /user/id/`:更新某个id的用户信息(只需要提供需要改变的信息)。
|
||||
- `DELETE /user/id/`:删除一个用户。
|
||||
|
||||
### 1.5 状态码:
|
||||
|
||||
| 状态码 | 原生描述 | 描述 |
|
||||
| :----- | :-------------------- | :----------------------------------------------------------- |
|
||||
| 200 | OK | 服务器成功响应客户端的请求。 |
|
||||
| 400 | INVALID REQUEST | 用户发出的请求有错误,服务器没有进行新建或修改数据的操作 |
|
||||
| 401 | Unauthorized | 用户没有权限访问这个请求 |
|
||||
| 403 | Forbidden | 因为某些原因禁止访问这个请求 |
|
||||
| 404 | NOT FOUND | 用户发送的请求的url不存在 |
|
||||
| 406 | NOT Acceptable | 用户请求不被服务器接收(比如服务器期望客户端发送某个字段,但是没有发送)。 |
|
||||
| 500 | Internal server error | 服务器内部错误,比如出现了bug |
|
||||
|
||||
## 2. Flask-Restful插件
|
||||
|
||||
### 2.1 介绍:
|
||||
|
||||
`Flask-Restful`是一个专门用来写`restful api`的一个插件。使用他可以快速的集成`restful api`功能。在`app`的后台以及纯`api`的后台中,这个插件可以帮助我们节省很多时间。当然,如果在普通的网站中,这个插件就显得有些鸡肋了,因为在普通的网页开发中,是需要去渲染HTML代码的,而`Flask-Restful`在每个请求中都是返回`json`格式的数据。
|
||||
|
||||
### 2.2 安装:
|
||||
|
||||
`Flask-Restful`需要在`Flask 0.8`以上的版本,在`Python2.6`或者`Python3.3`上运行。通过`pip install flask-restful`即可安装。
|
||||
|
||||
### 2.3 定义Restful的视图:
|
||||
|
||||
如果使用`Flask-Restful`,那么定义视图函数的时候,就要继承自`flask_restful.Resource`类,然后再根据当前请求的`method`来定义相应的方法。比如期望客户端是使用`get`方法发送过来的请求,那么就定义一个`get`方法。类似于`MethodView`。示例代码如下:
|
||||
|
||||
```python
|
||||
from flask import Flask,render_template,url_for
|
||||
from flask_restful import Api,Resource
|
||||
|
||||
app = Flask(__name__)
|
||||
## 用Api来绑定app
|
||||
api = Api(app)
|
||||
|
||||
class IndexView(Resource):
|
||||
def get(self):
|
||||
return {"username":"zhiliao"}
|
||||
|
||||
api.add_resource(IndexView,'/',endpoint='index')
|
||||
```
|
||||
|
||||
注意事项:
|
||||
|
||||
1. `endpoint`是用来给`url_for`反转`url`的时候指定的。如果不写`endpoint`,那么将会使用视图的名字的小写来作为`endpoint`。
|
||||
2. `add_resource`的第二个参数是访问这个视图函数的`url`,这个`url`可以跟之前的`route`一样,可以传递参数。并且还有一点不同的是,这个方法可以传递多个`url`来指定这个视图函数。
|
||||
|
||||
### 2.4 参数解析:
|
||||
|
||||
`Flask-Restful`插件提供了类似`WTForms`来验证提交的数据是否合法的包,叫做`reqparse`。以下是基本用法:
|
||||
|
||||
```python
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('username',type=str,help='请输入用户名')
|
||||
args = parser.parse_args()
|
||||
```
|
||||
|
||||
`add_argument`可以指定这个字段的名字,这个字段的数据类型等。以下将对这个方法的一些参数做详细讲解:
|
||||
|
||||
1. `default`:默认值,如果这个参数没有值,那么将使用这个参数指定的值。
|
||||
2. `required`:是否必须。默认为False,如果设置为`True`,那么这个参数就必须提交上来。
|
||||
3. `type`:这个参数的数据类型,如果指定,那么将使用指定的数据类型来强制转换提交上来的值。
|
||||
4. `choices`:选项。提交上来的值只有满足这个选项中的值才符合验证通过,否则验证不通过。
|
||||
5. `help`:错误信息。如果验证失败后,将会使用这个参数指定的值作为错误信息。
|
||||
6. `trim`:是否要去掉前后的空格。
|
||||
|
||||
其中的`type`,可以使用`python`自带的一些数据类型,也可以使用`flask_restful.inputs`下的一些特定的数据类型来强制转换。比如一些常用的:
|
||||
|
||||
1. `url`:会判断这个参数的值是否是一个url,如果不是,那么就会抛出异常。
|
||||
2. `regex`:正则表达式。
|
||||
3. `date`:将这个字符串转换为`datetime.date`数据类型。如果转换不成功,则会抛出一个异常。
|
||||
|
||||
### 2.5 输出字段:
|
||||
|
||||
对于一个视图函数,你可以指定好一些字段用于返回。以后可以使用ORM模型或者自定义的模型的时候,他会自动的获取模型中的相应的字段,生成`json`数据,然后再返回给客户端。这其中需要导入`flask_restful.marshal_with`装饰器。并且需要写一个字典,来指示需要返回的字段,以及该字段的数据类型。示例代码如下:
|
||||
|
||||
```python
|
||||
class ProfileView(Resource):
|
||||
resource_fields = {
|
||||
'username': fields.String,
|
||||
'age': fields.Integer,
|
||||
'school': fields.String
|
||||
}
|
||||
|
||||
@marshal_with(resource_fields)
|
||||
def get(self,user_id):
|
||||
user = User.query.get(user_id)
|
||||
return user
|
||||
```
|
||||
|
||||
在`get`方法中,返回`user`的时候,`flask_restful`会自动的读取`user`模型上的`username`以及`age`还有`school`属性。组装成一个`json`格式的字符串返回给客户端。
|
||||
|
||||
#### 2.5.1 重命名属性:
|
||||
|
||||
很多时候你面向公众的字段名称是不同于内部的属性名。使用 attribute可以配置这种映射。比如现在想要返回`user.school`中的值,但是在返回给外面的时候,想以`education`返回回去,那么可以这样写:
|
||||
|
||||
```python
|
||||
resource_fields = {
|
||||
'education': fields.String(attribute='school')
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.5.2 默认值:
|
||||
|
||||
在返回一些字段的时候,有时候可能没有值,那么这时候可以在指定`fields`的时候给定一个默认值,示例代码如下:
|
||||
|
||||
```python
|
||||
resource_fields = {
|
||||
'age': fields.Integer(default=18)
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.5.3 复杂结构:
|
||||
|
||||
有时候想要在返回的数据格式中,形成比较复杂的结构。那么可以使用一些特殊的字段来实现。比如要在一个字段中放置一个列表,那么可以使用`fields.List`,比如在一个字段下面又是一个字典,那么可以使用`fields.Nested`。以下将讲解下复杂结构的用法:
|
||||
|
||||
```python
|
||||
class ProfileView(Resource):
|
||||
resource_fields = {
|
||||
'username': fields.String,
|
||||
'age': fields.Integer,
|
||||
'school': fields.String,
|
||||
'tags': fields.List(fields.String),
|
||||
'more': fields.Nested({
|
||||
'signature': fields.String
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
21
04.Flask/09.Cookie和Session.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Cookie和Session
|
||||
|
||||
1. `cookie`:在网站中,http请求是无状态的。也就是说即使第一次和服务器连接后并且登录成功后,第二次请求服务器依然不能知道当前请求是哪个用户。`cookie`的出现就是为了解决这个问题,第一次登录后服务器返回一些数据(cookie)给浏览器,然后浏览器保存在本地,当该用户发送第二次请求的时候,就会自动的把上次请求存储的`cookie`数据自动的携带给服务器,服务器通过浏览器携带的数据就能判断当前用户是哪个了。`cookie`存储的数据量有限,不同的浏览器有不同的存储大小,但一般不超过4KB。因此使用`cookie`只能存储一些小量的数据。
|
||||
2. `session`: session和cookie的作用有点类似,都是为了存储用户相关的信息。不同的是,`cookie`是存储在本地浏览器,`session`是一个思路、一个概念、一个服务器存储授权信息的解决方案,不同的服务器,不同的框架,不同的语言有不同的实现。虽然实现不一样,但是他们的目的都是服务器为了方便存储数据的。`session`的出现,是为了解决`cookie`存储数据不安全的问题的。
|
||||
3. `cookie`和`session`结合使用:`web`开发发展至今,`cookie`和`session`的使用已经出现了一些非常成熟的方案。在如今的市场或者企业里,一般有两种存储方式:
|
||||
- 存储在服务端:通过`cookie`存储一个`session_id`,然后具体的数据则是保存在`session`中。如果用户已经登录,则服务器会在`cookie`中保存一个`session_id`,下次再次请求的时候,会把该`session_id`携带上来,服务器根据`session_id`在`session`库中获取用户的`session`数据。就能知道该用户到底是谁,以及之前保存的一些状态信息。这种专业术语叫做`server side session`。存储在服务器的数据会更加的安全,不容易被窃取。但存储在服务器也有一定的弊端,就是会占用服务器的资源,但现在服务器已经发展至今,一些`session`信息还是绰绰有余的。
|
||||
- 将`session`数据加密,然后存储在`cookie`中。这种专业术语叫做`client side session`。`flask`采用的就是这种方式,但是也可以替换成其他形式。
|
||||
|
||||
### flask中使用cookie和session
|
||||
|
||||
1. cookies:在`Flask` 中操作`cookie` ,是通过`response` 对象来操作,可以在`response` 返回之前,通过`response.set_cookie` 来设置,这个方法有以下几个参数需要注意:
|
||||
|
||||
- key:设置的cookie的key。
|
||||
- value:key对应的value。
|
||||
- max_age:改cookie的过期时间,如果不设置,则浏览器关闭后就会自动过期。
|
||||
- expires:过期时间,应该是一个`datetime`类型。
|
||||
- domain:该cookie在哪个域名中有效。一般设置子域名,比如`cms.example.com`。
|
||||
- path:该cookie在哪个路径下有效。
|
||||
|
||||
2. session:`Flask`中的`session`是通过`from flask import session`。然后添加值key和value进去即可。并且,`Flask`中的`session`机制是将`session`信息加密,然后存储在`cookie`中。专业术语叫做`client side session`。
|
||||
|