《Flask Web开发实战》笔记

定义视图函数及路由

基础

定义一个普通的视图函数及路由规则

1
2
3
4
5
6
7
from flask import Flask

app = Flask(__name__)

@app.route('/hello')
def hello():
return 'Hello World!'

动态路由匹配

1
2
3
@app.route('/hello/<string:name>')
def hello(name):
return 'Hello %s!' % name

其他类型转换器:

  • int 整型
  • float 浮点型
  • string 除/以外的字符串
  • path 包含/的字符串
  • uuid UUID字符串
  • any 匹配一系列给定值中的一个元素

any转换器

1
2
3
@app.route('/color/<any(blue, white, black):color>')
def choice_color(color):
return 'Your Choice %s!' % color

视图函数默认会监听GET/POST/HEAD的请求方法, 如果想限制视图函数只能通过某个方法访问可以通过methods参数限制, 其类型为一个列表。

1
@app.route('/hello', methods=['GET', 'POST'])

视图钩子

每个钩子可以处理多个函数, 函数名无需和钩子名一致。

钩子 说明
before_first_request 注册一个函数, 在处理第一次请求时运行
before_request 在处理每次请求书时运行
after_request 如果没有未处理的异常抛出则会在每个请求结束后运行
teardown_request 即时出现未处理的异常也会在每个请求结束后运行。如果发生异常则会把异常对象传入到注册的函数中
after_this_request 在视图函数内注册一个函数, 会在这个请求结束后运行

HTTP响应

视图函数的返回值可以自定义响应内容如指定响应状态码为201:

1
2
3
@app.route('/hello')
def hello():
return 'Hello!', 201

自定义响应状态码及响应头:

1
2
def follow():
return '', 302, {'Location': 'https://www.baidu.com'}

使用redirect函数重定向:

1
2
def hello():
return redirect('https://www.baidu.com')

redirect + url_for 重定向到其他视图:

1
2
def redirect_hello():
return redirect(url_for('hello'))

make_response函数

使用make_response函数生成响应对象

1
2
3
4
5
6
7
8
9
10
11
12
import json
from flask import Flask, make_response

@app.route('/foo')
def foo():
data = {
'name': 'Join',
'age': 21
}
response = make_response(json.dumps(data))
response.mimetype = 'application/json'
return response

jsonify函数简化上述操作:

1
2
def foo():
return jsonify({'name': 'Join', 'age': 21})

Response对象

方法/属性 说 明
headers WerkZeug 的 Headers对象, 表示响应头, 可以像字典操作
status 状态码, 文本型
status_code 状态码, 整型
mimetype MIME类型
set_cookie() 设置Cookie

Flask上下文变量

变 量 名 上下文类别 说 明
current_app 程序上下文 指向处理请求的上下文实例
g 程序上下文 替代Python 的全局变量用法, 确保仅在当前请求中可用。用于存储全局数据, 每次请求都会重置
request 请求上下文 封装客户端发出的请求报文数据
session 请求上下文 用于记住请求之间的数据, 通过签名的Cookie实现

上图四个对象为真实对象的代理对象, 即通过代理对象(Proxy Object)访问真实对象。如果需要获取真实对象可以通过调用代理对象的_get_current_object()方法获取。

模板内置上下文

Flask中使用的模板引擎为JinJa2, Flask在模板上下文中提供了一些内置变量, 可以在模板中直接使用。

变 量 说 明
config 当前的配置对象
request 当前的请求对象, 在已激活的请求环境下可用
sessio 当前的会话对象, 在已激活的请求环境下可用
g 与请求绑定的全局变量, 在已激活的请求环境下可用

模板自定义上下文

如果多个模板需要使用同一变量时, 避免在视图函数中重复传递变量, 更好的方法是设置一个模板全局变量。Flask提供了一个app.context_processor装饰器, 可以用来注册模板上下文处理函数。它可以帮我们完成统一传入变量的工作。模板上下文处理函数需要返回一个键值对的字典。

1
2
3
4
@app.context_processor
def inject_foo():
foo = 'I am foo'
return dict(foo=foo)

当调用render_template渲染任意一个模板时, 所有使用app.context_processor装饰器注册的模板上下文处理函数都会被执行。这些函数的返回值会被添加到模板中可直接使用。

除了装饰器的方法以外, 你也可以直接调用该函数传入对象以注册。

1
2
3
def inject_foo():
return dict(foo=foo)
app.context_processor(inject_foo)

JinJa2模板引擎全局函数

全局函数列表

Flask内置的模板全局函数

函 数 说 明
url_for() 生成URL
get_flashed_messages 用于获取flash消息

自定义模板全局函数

方式一:app.template_global装饰器

1
2
3
@app.template_global
def foo():
return 'I am foo'

方式二:app.add_template_global函数

1
2
# 可选的name参数
app.add_template_global(func[,name])

过滤器

在Jinja2中,过滤器(filter)是一些可以用来修改和过滤变量值的特殊函数,过滤器和变量用一个竖线(管道符号)隔开,需要参数的过滤器可以像函数一样使用括号传递。下面是一个对name变量使用title过滤器的例子:

1
{{ name|title }}

另一种用法是使用标签声明

1
2
3
{% filter upper %}
This text becomes uppercase.
{% endfilter %}

完整过滤器列表

同时使用多个过滤器

1
<h1>Hello, {{ name|default('默认值')|title }}!</h1>

不对变量进行转义的两种方法:

1
{{ sanitized|safe }}

转换为Markup对象:

1
2
3
4
5
6
from flask import Flask, Markup

@app.route('/hello')
def hello():
text = Markup('<h1>Hello, Flask!</h1>')
return render_template('index.html', text=text)

自定义过滤器

使用app.template_filter装饰器可以注册自定义过滤器。

1
2
3
4
5
from flask import Markup

@app.template_filter
def musical(s):
return s + Markup('&#9835;')

和注册全局函数类型, 可以使用name可选参数为过滤器命名, 默认则为函数名称。

测试器

测试器(Test)可用于测试一些变量或表达式, 返回布尔值类型(True or False)的特殊函数。例如使用number测试一个变量是否为数字。

1
2
3
4
5
{% if age is number %}
{{ age * 365 }}
{% else %}
无效数字
{% endif %}

完整测试器列表

如果测试器包含多个参数则可以使用如下两种形式:

1
2
3
4
# One
{% if foo is sameas(bar) %}
# Two
{% if foo is sameas bar %}

自定义注册器

跟前面的步骤一样, 代码:

1
2
3
4
5
@app.template_test()
def baz(n):
if n == 'baz':
return True
return False

模板环境对象

在Jinja2中,渲染行为由jinja2.Enviroment类控制,所有的配置选项、上下文变量、全局函数、过滤器和测试器都存储在Enviroment实例上。当与Flask结合后,我们并不单独创建Enviroment对象,而是使用Flask创建的Enviroment对象,它存储在app.jinja_env属性上。

在程序中,我们可以使用app.jinja_env更改Jinja2设置。比如,你可以自定义所有的定界符。下面使用variable_start_string和variable_end_string分别自定义变量定界符的开始和结束符号:

1
2
3
app = Flask(__name__)
app.jinja_env.variable_start_string = '[['
app.jinja_env.variable_end_string = ']]'

模板环境中的全局函数、过滤器和测试器分别存储在Enviroment对象的globals、filters和tests属性中,这三个属性都是字典对象。除了使用Flask提供的装饰器和方法注册自定义函数,我们也可以直接操作这三个字典来添加相应的函数或变量,这通过向对应的字典属性中添加一个键值对实现,传入模板的名称作为键,对应的函数对象或变量作为值。下面是几个简单的示例。

添加自定义注册对象

1
2
3
4
5
def bar():
return 'I am bar.'
foo = 'I am foo'
app.jinja_env.globals['bar'] = bar
app.jinja_env.globals['foo'] = foo

添加自定义过滤器

1
2
3
def smiling(s):
return s + ' :)'
app.jinja_env.filters['smiling'] = smiling

添加自定义测试器

1
2
3
4
5
def baz(n):
if n == 'baz':
return True
return False
app.jinja_env.tests['baz'] = baz

Enviroment

局部模板

通常在Web应用程序中会出现很多模板代码复用的问题, 例如需要在多个模板中显示另一个模板的内容(假设为首页模板)此时如果在每个模板中都重复编写代码则会造成冗余且不易维护。因此可以定义一个局部模板, 当多个模板需要使用相同的内容时只需包含该模板即可。

Tip: 为了区分局部模板, 通常使用前导下划线命名

1
{% include '_banner.html' %}

模板继承

简单理解为局部模板的Plus版就好了。

编写基模板, 使用block和endblock标签声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
{% block head %}
<meta charset="utf-8">
<title> {% block title %}Template - HelloFlask {% endblock title %}</title>
{% endblock head %}

<nav>
<ul><li><a href="{{ url_for('index') }}">Home</a></li></ul>
</nav>
<main>
{% block context %}

{% endblock context %}
</main>
</html>

当子模板继承基模板后, 子模板会自动包含基模板的内容和结构, 通过声明标签的形式可以修改基模板中的内容。如果在子模板中修改了基模板定义的块则会覆盖。

1
2
3
4
5
{% extends 'base.html' %}
<h1>Template</h1>
{% block context %}
<p>DoSomethink</p>
{% endblock context %}

追加内容(不会覆盖基模板中原先定义的)

1
2
3
4
{% block context %}
{{ super() }}
<p>Chaned Somethink</p>
{% endblock context %}

加载静态文件

在Flask项目的根目录下新建static文件夹, 便可使用url_for函数生成静态文件的URL。

1
2
# 假设 app/static/avatar.jpg文件存在
url_for('static', filename='avatar.jpg')

而在模板文件中引入CSS则可以通过如下方式:

1
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">

消息闪现

在视图函数中通过flash函数传递一个闪现消息, 该消息会存储在session中, 在模板文件中可以通过循环get_flashed_message函数的返回值获取消息。当调用该函数时存储在session中的消息便会销毁。

1
2
3
4
5
6
app.config['SECRET_KEY'] = os.urandom(24)

@app.route('/flash')
def just_flash():
flash('I am flash, who is looking for me?')
return redirect(url_for('index'))

模板文件:

1
2
3
{% for message in get_flashed_message() %}
<div class="alert">{{ message }}<div>
{% endfor %}

自定义错误页面

使用app.errorhandler装饰器传入指定的错误状态码装饰视图函数, 视图函数中需要接收异常对象。

1
2
3
@app.errorhandler(404)
def page_not_found(e):
return render_template('errors/404.html'), 404

异常对象常用属性

属 性 说 明
code 状态码
name 原因短语
description 错误描述, 另外使用get_description()还可以获取HTML格式的错误描述的代码

Flask-WTF处理表单

安装拓展pip install flask_wtf

字段类 说明 对应的HTML表示
BooleanField 复选框, 值为True或False
DateField 文本字段, 值会被处理为datetime.date对象
DateTimeField 文本字段, 值会被处理为datetime.datetime对象
FileFiled 文件上传字段
FloatField 浮点数字段, 值会被处理为浮点型
IntegerField 整数字段, 值会被处理为整数型
RadioField 一组单选按钮
SelectField 下拉列表
SelectMultipleField 多选下拉列表
SubmitField 提交按钮
StringField 文本字段
HiddendField 隐藏文本字段
PasswordField 密码文本字段
TextAreaFiled 多行文本字段

字段类常用参数

参数 说明
label 字段标签
render_kw 一个字典, 用于设置对应的标签的属性
validators 列表类型, 包含一系列验证器
default 字符串或可调用对象, 为表单字段设置默认值

WTForms验证器

验证器 说明
DataRequired(message=None) 验证数据是否有效
Email(message=None) 验证Email地址
EqualTo(fieldname, message=None) 验证两个字段值是否相同
InputRequired(message=None) 验证是否有数据
Length(min=-1, max=-1, message=None) 验证输入值是否在给定范围内
NumerRange(min=None, max=None, message=None) 验证数字是否在给定范围内
Optional(strip_whitespace=True) 允许输入值为空, 并跳过其他验证
Regexp(regex, flags=0, message=None) 使用正则验证输入值
URL(require_tld=True, message=None) 验证URL
AnyOf(values, message=None, values_formatter=None) 确保输入值在可选值列表值中
NoneOf(values, message=None, values_formatter=None) 确保输入值不在可选值列表中

定义一个表单类, 实例化对象传入模板文件中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from wtforms import Form, StringField, PasswordField, SubmitField, BooleanField
from wtforms.validators import DataRequired, Length

class LoginForm(Form):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired(), length(8, 120)])
remember = BooleanField('Rememeber me')
submit = SubmitField('Log in')

# 实例化对象 调用方法生成HTML
@app.route('/basic')
def basic():
form = LoginForm()
return render_template('login.html', form=form)
1
2
3
4
5
6
7
8
<!-- login.html -->
<form method="POST">
{{ form.csrf_token }}
{{ form.username }}<br/>
{{ form.password }}<br/>
{{ form.remember }}<br/>
{{ form.submit }}
</form>

使用validate_on_submit函数判断表单字段是否通过验证, 验证成功则返回True, 反则会将错误消息添加到表单类的errors属性中。

1
2
3
4
5
6
7
8
@app.route('/basic', methods=['GET', 'POST'])
def basic():
form = LoginForm()
if form.validate_on_submit():
username = form.username.data
flash('Welcome Home, %s!' % username)
return redirect(url_for('index'))
return render_template('login.html', form=form)

验证不通过打印错误消息

1
2
3
{% for message in form.username.errors %}
<font color="red">{{ message }}</font>
{% endfor %}

使用宏渲染表单字段, 可节省代码工作, 让其接收一个field字段对象和关键字参数。

1
2
3
4
5
6
7
8
9
10
<!-- macros.html -->
{% macro form_field(field) %}
{{ field.label }}<br/>
{{ field.(**kwargs) }}
{% if field.errors %}
{% for message in field.errors %}
<font color="red">message</font>
{% endfor %}
{% endif %}
{% endmacro %}

在模板文件中使用宏:

1
2
3
4
5
6
{% from 'macro.html' import form_field %}
<form method="POST">
{{ form.csrf_token }}
{{ form_field(form.username) }}
{{ form_field(form.password) }}
</form>

自定义验证器

1
2
3
4
5
6
7
8
9
from flask_wtf import FlaskForm
from wtforms.validators import ValidationError

class FortyTwoForm(FlaskForm):
answer = IntegerField('The Number')
submit = SubmitField()
def validate_answer(form, field):
if field.data != 42:
return ValidationError('Must be 42.')

自定义通用验证器

1
2
3
4
5
6
7
8
9
10
11
12
from wtforms.validators import ValidationError

def is_42(message=None):
if message is None:
message = 'Must be 42.'
def _is_42(form, field):
if field != 42:
raise ValidationError(message)
return _is_42

class Form(FlaskForm):
answer = IntegerField('The Number', validators=[is_42()])

上传表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import uuid
from flask import Flask, send_from_directory
from flask_wtf import FlaskForm

def random_filename(filename):
ext = os.path.splitext(filename)[1]
return uuid.uuid4().hex + ext

class UploadForm(FlaskForm):
photo = FileField('Image', validators=[FileRequired(), FileAllowed(['jpg', 'png', 'gif'])])
submit = SubmitField('Submit')

@app.route('uploads/<path:filename>')
def get_file(filename):
return send_from_directory(app.config['UPLOAD_PATH'], filename)

@app.route('/show_images')
def show_images():
return render_template('uploaded.html')

@app.route('/upload', methods=['GET', 'POST'])
def upload_image():
form = UploadForm()
if form.validate_on_submit():
f = form.photo.data
filename = random_filename(f.filename)
f.save(os.path.join(app.config['UPLOAD_PATH'], filename))
session['filename'] = [filename]
return redirect(url_for('show_images'))
return render_template('upload.html', form=form)

模板文件uploaded.html

1
2
3
4
5
6
7
8
9
10
<html>
<head>
<meta charset="utf-8">
<title>Show Images</title>
</head>

<body>
<img src="{{ url_for('get_file', filename=session.filename)}}" alt="">
</body>
</html>

SQLAlchemy拓展

安装拓展

1
pip3 install flask_sqlalchemy

配置数据库URI及实例化:

1
2
3
import os

app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite:///' + os.path.join(app.root_path + 'data.db'))

定义数据库模型

模型类对应数据库中的表, 需继承自db.Model基类。

1
2
3
4
5
6
# __tablename__属性可指定表名, 默认为类名
class Node(db.Model):
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.Text)

db.create_all()

SQLAlchemy常用字段类型

字段 说明
Integer 整数
String 字符串, 可选参数length可设置最大长度
Text 较长的Unicode文本
Date 存储Python的datetime.date对象
Time datetime.time对象
DateTime datetime对象
Interval datetime.timedelta对象
Float 浮点数
Boolean 布尔值
PickleType 存储Pickle列化的Python对象
LargeBinary 存储二进制数据

db.Column类参数

参数名 说明
primary_key 设为True则该字段为主键
unique 设为True则该字段内容不可重复
index 设为True则为该字段设置索引, 提高查询效率
nullable 设为True则字段可为空
default 为字段设置默认值
***

SQLAlchemy查询方法, <模型类>.query.<过滤方法>.<查询方法>

查询方法 说明
all() 返回包含所有查询记录的列表
first() 返回查询的第一条记录, 如果未找到则返回None
one() 返回第一条记录, 且只允许有一条记录。不为一条则抛出错误
get(ident) 传入主键的值作为参数, 返回指定主键值的记录。未找到则为None
count() 返回查询结果集的数量
one_or_none() 类似one, 结果集不为1则返回None
first_or_404() 返回查询的第一条记录, 未找到则返回404响应
get_or_404(ident) 同上
paginate() 返回一个Paginate对象, 可对记录分页处理

SQLAlchemy常用过滤方法

过滤器名称 说明
filter() 使用指定的规则过滤, 返回新产生的查询对象
filter_by() 使用指定规则过滤记录(以关键字表达式的形式), 返回新产生的查询对象
order_by() 根据指定条件对记录进行排序, 返回新产生的查询对象
limit(limit) 使用指定的值限定原查询记录数量, 返回新产生的查询对象
group_by() 根据指定条件对记录进行分组, 返回新产生的查询对象
offset(offset) 使用指定的值偏移原查询的结果集, 返回新产生的查询对象

使用filter过滤器查询body字段为SHAVE的记录。filter过滤器还可以使用如like, in_, not in, and, or等条件。

1
2
3
4
5
6
7
8
9
10
11
Note.query.filter(Note.body=='SHAVE').first()
# 打印SQL语句
print(Note.query.filter(Note.body=='SHAVE'))
# like
Note.query.filter(Note.body.like('%foo%')).first()
# in
Note.query.filter(Note.body.in_(['foo', 'bar'])).first()
# not in
Note.query.filter(~Note.body.in_['foo', 'bar']).first()
# and
Note.query.first(and_(Note.body == 'foo', Note.title == 'Foo'))

更新记录

1
2
3
note = Note.query.get(2)
note.body = 'New Content'
db.session.commit()

删除记录

1
2
3
note = Note.query.get(2)
db.session.delete(note)
db.session.commit()

实现添加新笔记视图

模型类:app/models/NoteModel.py

1
2
3
4
5
6
from flask_sqlalchemy import SQLAlchemy

class NoteModel(db.Model):
__tablename__ = 'note'
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.Text)

表单类:app\forms\NoteForm.py

1
2
3
4
5
6
7
from flask_wtf import FlaskForm
from wtforms import TextAreaField, SubmitField
from wtforms.validators import DataRequired

class NoteForm(FlaskForm):
body = TextAreaField('Body', validators=[DataRequired()])
submit = SubmitField('Save')

应用程序:app\app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import  os
from flask import Flask, flash, render_template,
redirect, url_for
from models import NoteModel

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite:///' + os.path.join(app.root_path, 'data.db'))
app.config['SECRET_KEY'] = os.urandom(24)

db = SQLAlchemy(app)

@app.route('/new', methods=['GET', 'POST'])
def new_note():
form = Note()
if form.validate_on_submit():
body = form.body.data
note = NoteModel(body=body)
db.session.add(note)
db.session.commit()
flash('Your Note Saved.')
return redirect(url_for('index'))
return render_template('new_note.html', form=form)

模板:new_note.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html>
<head>
<meta charset="utf-8">
<title>New Note</title>
</head>

<body>
<h2>New Note</h2>
<form action="#" method="POST">
{{ form.csrf_token }}
{{ form.body.label }}
{{ form.body(rows=5, cols=50) }}<br/>
{{ form.submit }}
</form>
</body>
</html>

index视图:

1
2
3
@app.route('/index')
def index():
return render_template('index.html')

模板:index.html

1
2
3
4
5
6
7
8
9
10
11
<html>
<head>
<meta charset="utf-8">
<title>Note Book</title>
</head>

<body>
<h1>Note Book</h1>
<a href="{{ url_for('new_note') }}">New Note</a>
</body>
</html>

程序执行流程

客户端(浏览器)访问路由http://127.0.0.1/new, new_note视图函数开始执行。 实例化Note类, 定义表和字段类型。当表单的提交按钮被点击时则form.validate_on_submit方法返回True, 进入分支代码块。 获取表单输入的内容后添加到数据库中, 然后跳转到index视图。

修改首页视图和模板文件

1
2
3
4
@app.route('/index')
def index():
notes = Node.query.all()
return render_template('index.html', notes=notes)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html>
<head>
<meta charset="utf-8">
<title>Note Book</title>
</head>

<body>
<h1>Note Book</h1>
<a href="{{ url_for('new_note') }}">New Note</a>
<h4>{{ notes|length }} notes:</h4>
{% for note in notes %}
<div>
<p>ID: {{ note.id }} - 内容: {{ note.body }}</p>
</div>
{% endfor %}
</body>
</html>

编辑笔记内容

编辑表单:app\forms\EditNote.py

1
2
class EditNoteForm(Note):
submit = SubmitField('Update')

编辑视图:app\app.py

1
2
3
4
5
6
7
8
9
10
@app.route('/edit/<int:note_id>', methods=['GET', 'POST'])
def edit_note(note_id):
form = EditNoteForm()
note = Note.query.get(note_id)
if form.validate_on_submit():
note.body = form.body.data
db.session.commit()
return redirect(url_for('index'))
form.body.data = note.body
return render_template('edit_note.html', form=form)

模板文件:edit_note.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html>
<head>
<meta charset="utf-8">
<title>Note Book</title>
</head>

<body>
<h2>Edit Note</h2>
<form action="#" method="POST">
{{ form.csrf_token }}
{{ form.body.label }}
{{ form.body }}<br/>
{{ form.submit }}
</form>
</body>
</html>

删除笔记

删除表单:

1
2
class DeleteNoteForm(FlaskForm):
submit = SubmitField('Delete')

删除视图:

1
2
3
4
5
6
7
8
9
10
@app.route('/delete/<int:note_id>', methods=['POST'])
def delete_note(note_id):
form = DeleteNoteForm()
if form.validate_on_submit():
note = Note.query.get(note_id)
db.session.delete(note)
db.session.commit()
else:
abort(404)
return redirect(url_for('index'))

在首页视图中实例化删除表单对象传入模板:

1
2
3
4
5
@app.route('/index')
def index():
note = Note.query.all()
form = DeleteNoteForm()
return render_template('index.html', note=note, form=form)

一对多表关系示例

以作者表和文章表演示数据库一对多关系, 将Article表的author_id字段作为外键关联Author表的id

1
2
3
4
5
6
7
8
9
10
class Author(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(70), unique=True)
phone = db.Column(db.String(20))

class Article(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(50), index=True)
body = db.Column(db.Text)
author_id = db.Column(db.Integer, db.ForeignKey('author.id'))

定义关系属性:

1
2
class Author(db.Model):
articles = db.relationship('Article')

添加数据并关联表:

1
2
3
4
5
6
7
8
9
10
11
12
# flask shell 你可以像列表一样操作articles字段
>>> foo = Author(name='Foo', phone='18888888888')
>>> spam = Article(title='Spam')
>>> ham = Article(title='Ham')
>>> db.session.add(foo)
>>> db.session.add(spam)
>>> db.session.add(ham)
# 第一种方式
>>> spam.author_id(1)
# 第二种方式
>>> foo.articles.append(ham)
>>> db.session.commit()

relationship关系函数参数

参数名 说明
back_populates 定义反向引用, 用于建立双向关系。在关系的另一侧也必须显示定义属性
backref 添加反向引用, 自动在另一侧建立关系属性。是back_populates的简版
lazy 指定如何加载相关记录
uselist 指定是否使用列表的形式加载记录, 设为False则使用标量(scalar)
cascade 设置级联操作
order_by 指定加载相关记录时的排序方式
secondary 在多对多关系中指定关联表
primaryjoin 指定多对多关系中的一级联结条件
secondaryjoin 指定多对多关系中的二级联结条件

上述表格中lazy参数的可选值

关系加载方式 说明
select 在必要时一次性加载记录, 返回包含记录的列表, 等于lazy=True
joined 和父查询一样加载记录, 但使用联结, 等于lazy=False
immediate 一旦父查询加载就加载
subquery 类似于joined, 但使用子查询
dynamic 不直接加载记录, 而是返回一个包含相关记录的query对象, 以便再继续附加查询函数对结果进行过滤

一对多关系的双向关系示例, 每本书对应多个作者, 每个作者对应多本书。

1
2
3
4
5
6
7
8
9
10
class Writer(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(70), unique=True)
books = db.relationship('Book', back_populates='writer')

class Book(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(50), index=True)
writer_id = db.Column(db.Integer, db.ForeignKey('writer.id'))
writer = db.relationship('Writer', back_populates='books')

设置双向关系后, 即可通过将某个Writer对象赋值给Book对象的writer属性建立关系。

1
2
3
4
5
6
7
8
9
10
>>> king = Writer(name='Stephen King')
>>> carrie = Book(title='Carrie')
>>> it = Book(title='IT')
>>> db.session.add(king)
>>> db.session.add(carrie)
>>> db.session.add(it)
>>> db.session.commit()
# 建立关系, 解除关系只需将标量属性设为None即可
>>> carrie.writer = king
>>> king.books

实战项目-SayHello

目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.
├── LICENSE
├── Pipfile
├── Pipfile.lock
├── README.md
├── requirements.txt
├── sayhello
│   ├── __init__.py
│   ├── commands.py
│   ├── errors.py
│   ├── forms.py
│   ├── models.py
│   ├── settings.py
│   ├── static
│   ├── templates
│   └── views.py
└── test_sayhello.py

首先编写项目的包构造文件, 实例化Flask类并导入全局配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# settings.py
import sys
import os
from sayhello import app

platform = sys.platform.startswith('win')
# 判断操作系统
if platform:
prefix = 'sqlite:///'
else:
prefix = 'sqlite:////'

SECRET_KEY = os.urandom(24) # 密钥
SQLALCHEMY_DATABASE_URI = prefix + os.path.join(os.path.dirname(app.root_path), 'data.db') # 数据库路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# __init__.py
from flask import Flask
from flask_bootstrap import Bootstrap
from flask_sqlalchemy import SQLAlchemy
from flask_moment import Moment

app = Flask(__name__)
app.config.from_pyfile('settings.py')
app.jinja_env.rstrip_blocks = True

bootstrap = Bootstrap()
db = SQLAlchemy()
moment = Moment()

from sayhello import views, errors, commands

下面编写模型类并注册flask命令:

1
2
3
4
5
6
7
8
9
# models.py
from datetime import datetime
from sayhello import app, db

class Message(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20))
body = db.Column(db.String(200))
timestamp = db.Column(db.DateTime, default=datetime.utcnow, index=True)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# commands.py
import click
from faker import Faker
from sayhello import db
from sayhello.models import Message

@app.cli.command()
@click.option('--drop', is_flag=True, help='清空数据表生成虚拟数据')
def forge(drop):
db.drop_all()
db.create_all()
click.echo('数据已清空!')
fake = Faker('zh_CN')
for data in range(20):
message = Message(
name = fake.name(),
body = fake.sentence(),
timestamp = fake.date_time_this_year()
)
db.session.add(message)
db.session.commit()
click.echo('已生成虚拟数据添加至数据库中!')

运行flask forge命令即可生成20条虚假数据至数据库中, 下面编写表单及视图类:

1
2
3
4
5
6
7
8
9
# forms.py
from flask_wtf import FlaskForm
from wtforms.valiadtors import DataRequired, Length
from wtforms import StringField, TextAreaField, SubmitField

class HelloForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), Length(1,20)])
body = TextAreaField('Message', validators=[DataRequired, Length(1,200)])
submit = SubmitField()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# views.py
from flask import render_template, redirect, url_for, flash
from sayhello import app, db
from sayhello.forms import HelloForm
from sayhello.models import Message

@app.route('/')
def index():
form = HelloForm()
message = Message.query.order_by(Message.timestamp.desc()).all()
if form.validate_on_submit():
name = form.name.data
body = form.body.data
message = Message(
name = name,
body = body
)
db.session.add(message)
db.session.commit()
flash('你的留言已发送至全世界!')
return redirect(url_for('index'))
return render_template('index.html', form=form, messages=messages)

蓝图

Blueprint 是一种组织一组相关视图及其他代码的方式。与把视图及其他 代码直接注册到应用的方式不同,蓝图方式是把它们注册到蓝图,然后在工厂函数中 把蓝图注册到应用。 – Flask官方文档


注册博客前台蓝图:

1
2
3
from flask import Blueprint

blog_bp = Blueprint('blog', __name__)

在实例化蓝图时可以指定url_prefixsubdomain, 在对应的视图函数中则会自动添加这些前缀无需声明。

1
2
3
4
5
6
7
from flask import Blueprint

auth_bp = Blueprint('auth', __name__, url_prefix='auth')

"""
admin_bp = Blueprint('admin', __name__, subdomain='admin')
"""

Blueprint API Documentation