5.1 SQL数据库

表中有个特殊的列,称为主键,其值是表中各行的唯一标识符,表中还可以有称为外键的列,引用同一个表或不同表中某行的主键,行之间的这种联系称为关系,这是SQL数据库的基础。

5.2 NoSQL数据库

所有不遵循上节所述关系模型的数据库统称为NoSQL数据库。NoSQL数据库一般使用集合Collection代替表,使用文档Document代替记录(行)。

NoSQL数据库可以减少表的数量,但却增加了数据重复量,但数据重复又可以提高查询速度。

5.3 使用SQL还是NoSQL

视实际情况所需。

5.4 Python数据库框架

数据库包:MySQL、Postgres、SQLite、Redis、MongoDB、CouchDB
数据库抽象层代码包(ORM或ODM):SQLAlchemy、MongoEngine

一般情况下,ORM和ODM对生产率的提升远远超过“把对象业务转换成数据库业务”而带来的性能降低。

5.5 使用Flask-SQLAlchemy管理数据库

在Flask-SQLAlchemy中,使用哪种数据库,要通过URL指定。常用的数据库引擎采用的数据库URL格式如下:

表5-1 Flask-SQLAlchemy数据库URL

数据库引擎 URL
MySQL mysql://username:password@hostname/database
Postgres postgres://username:password@hostname/database
SQLite(Unix) sqlite:////absolute/path/to/database
SQLite(Windows) sqlite:///c:/absolute/path/to/database

hostname可以是本地主机(localhost),也可以是原创服务器。
database表示要使用的数据库名称。

程序使用的数据库URL必须保存到Flask配置对象的SQLALCHEMY_DATABASE_URI键中。
配置对象中有个SQLALCHEMY_COMMIT_ON_TEARDOWN键,将其设为True时,每次请求结束后都会自动提交数据库中的变动(相当于自动commit)。

初始化及配置SQLite如下:

1
2
3
4
5
6
7
8
9
10
11
12
form flask_sqlalchemy import SQLAlchemy
import os
# ...
# 获取文件当前文件父路径
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI`] = 'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config[`SQLALCHEMY_COMMIT_ON_TEARDOWN`] = True
db = SQLAlchemy(app)

db对象是SQLAlchemy类的实例,表示程序所使用的数据库。

5.6 定义模型

在ORM中,模型(相当于数据库表table)一般是个Python类,类中的属性对应数据库表中的列。

在hello.py中定义Role模型和User模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Role(db.Model):
__tablename__ 'roles'
id = db.Column(db.Integer, primary_key=True)
role_name = db.Column(db.String(64), unique=True)
def __repr__(self):
return '<Role {}>'.format(self.role_name)
def User(db.Model):
__tablename__ = 'users'
id = db.Column(db.integer, primary_key=True)
user_name = db.Column(db.String(64), unique=True, index=True)
def __repr__(self):
return '<User {}>'.format(self.user_name)

类变量__tablename__定义在数据库中使用的表名,如果没有定义,Flask-SQLAlchemy会使用一个默认名字,但该名字没有遵守使用复数形式进行命名的约定。

db.Column类构造函数的第一个参数是数据库列的类型(如Integer、String等),对应模型(即Python类)的对象类型(如int、str等)

表5-2 最常用的SQLAlchemy列类型

类型名称 对应Python类型 说明
Integer int 普通整数,一般时32位
Float int 浮点数
String str 变长字符串
Text str 变成字符串,对较长或不限长度的字符串做了优化
Unicode unicode 变长Unicode字符串
UnicodeText unicode 变长Unicode字符串,对较长货不限长度的字符串做了优化
Date datetime.data 日期
Time datetime.time 时间
DateTime datetime.datetime 日期和时间

db.Column中其余的参数可对列中的数据做一些配置或设置

表5-3 最常用的SQLAlchemy列选项

选项名 说明
primary_key 如果设为True,这列是主键
unique 如果设为True,这列不允许出现重复值
index 如果设为True,为这列创建索引,提高查询效率
nullable 如果设为True,这列允许使用空值;设为False,则不允许为空
default 为这列设置默认值

注意:Flask-SQLAlchemy要求每个模型都要定义主键。

5.7 关系

定义一对多关系(Role对User):

1
2
3
4
5
6
7
class Role(db.Model):
# ...
users = db.relationship('User', backref='role')
class User(db.Model):
# ....
role_id = db.Column(db.Integer, db.ForeignKey('roles.id') # 外键:roles表中的id

添加到Role模型中的类变量users代表这个关系(Role与User的关系)的面向对象视角,它将返回与具体Role实例相关联的用户(具体User实例)组成的列表。(如Role实例role_admin返回的users为[richard, john],其中richardjohn都是User实例,即role_admin.users = [richard,john]体现了面向对象视角的一对多关系)

db.relationship()的第一个参数是这个关系的另一端对应的模型。如果模型尚未定义,可使用字符串形式指定。backref参数为'role'表明:在User模型中添加一个属性(或称为类变量)role(可理解成在User表中添加一列名为role的列。但实际查看数据库表时是看不到的),从而定义反向关系。
注意:一般情况下,db.relationship()都能自行找到关系中的外键,有在某些情况下无法决定把哪一列作为外键。如User模型中有两列或以上的列定义为Role模型的外键,那么SQLAlchemy就不知道该使用哪列了。此时,你就要为db.relationship()提供额外参数,从而确定使用哪列外键。常用配置如表5-4。

表5-4 db.relationship()常用的SQLAlchemy关系选项

选项名 说明
backref 在关系的另一端模型中添加反向作用
primaryjoin 明确指定两个模型之间使用的联结条件。只在模凌两可的关系中需要指定
lazy 指定如何加载相关记录。可选值有:select(首次访问时按需加载),immediate(源对象加载后加载),joined(加载记录,但使用联结),subquery(立即加载,但使用子查询),noload(永不加载),dynamic(不加载记录,单提供加载记录的查询)
userlist 如果设为Fales,不适用列表,而使用标量值
order_by 指定关系中记录的排序方式
secondary 指定多对多关系中关系表的名字
secondaryjoin SQLAlchemy无法自行决定时,指定多对多关系中的二级联结条件

一对一关系:可以用前面介绍的一对多关系表示,但调用db.relationship()时要把userlist设为False
多对一关系:可以用一对多关系表示,只是两个表对调。或者把外键db.relationship()都放在“多”这一侧。
多对多关系:需要用到第三张表(关系表)。

5.8 数据库操作

需要在Python Shell中进行操作。

5.8.1 创建表

1
2
3
(venv)$ python hello.py shell
>>> from hello.py import db
>>> db.create_all()

如果修改模型(如增加了一列)后要把修改的地方应用到现在的数据库中,那么更新现有数据库表的粗暴方式是先删除旧表,在重新创建表:

1
2
>>> db.drop_all()
>>> db.create_all()

但是这种方法会把数据库中原有的数据都删除掉。在5.11 使用Flask-Migrate实现数据库迁移中会介绍更新数据库更好的方式。

5.8.2 插入行

1
2
3
4
5
6
7
>>> from hello import Role, User
>>> role_admin = Role(role_name='admin')
>>> role_moderator = Role(role_name='moderator')
>>> role_visitor = Role(role_name='visitor')
>>> user_richard = User(user_name='richard')
>>> user_john = User(user_name='john')
>>> user_david = User(user_name='david')

现在这些对象只存在于Python中,还没有写入数据库。因此id尚未赋值,所以print(role_admin.id)的结果为None

要把对象写入数据库要分两步:

  1. 要把对象添加到会话(db.session)中:
    1
    2
    >>> db.session.add(role_admin)
    >>> db.session.add(user_richard)

或者简写成:

1
>>> db.session.add_all([role_moderator, role_visitor, user_john, user_david])

  1. 调用commit()提交会话
    1
    >>> db.session.commit()

此时对象已经写入数据库,再print(role_admin.id)时,它的结果时1

调用db.session.rollback()后,可实现事务回滚。添加到数据库会话中的所有对象都会还原为它们在数据库时的状态。

注意:如果在写入会话过程中发生了错误,那么整个会话都会实效。这样就保证了数据库的一致性。因为可以防止只更新正确部分,而发生错误的部分没有更新。

5.8.3 更新行

在数据库会话上调用add()方法可以更新数据。如:

1
2
3
>>> role_admin.role_name = 'administrator'
>>> db.session.add(role_admin)
>>> db.session.commit()

5.8.4 删除行

在数据库会话上调用delete()方法删除数据。如:

1
2
>>> db.session.delete(role_moderator)
>>> db.session.commit()

注意:插入、更新、删除,只有commit()后才会真正写入数据库。

5.8.5 查询行

Flask-SQLAlechemy为每个模型(注意不是具体的模型实例)都提供了query对象,在query对象上调用相应方法可进行查询。如查询所有记录:

1
2
>>> Role.query.all()
>>>[<Role 'admin'>, <Role 'visitor'>]

query对象上可使用过滤器进行更精确的查询,如:

1
2
>>> User.query.filter_by(role=role_visitor).all()
>>> [<User 'john'>, <User 'david'>]

表5-5 常用的SQLAlchemy查询过滤器

过滤器 说明
filter() 把过滤器添加到原查询上,返回一个新查询
filter_by() 把等值过滤器添加到原查询上,返回一个新查询
limit() 使用指定的值限制原查询返回的结果数量,返回一个新查询
offset() 偏移原查询返回的结果,返回一个新查询
order_by() 根据指定条件对原查询结果进行排序,返回一个新查询
group_by() 根据指定条件对原查询结果进行分组,返回一个新查询

注意:在查询上应用指定过滤器后,需通过一些执行函数执行查询。

表5-6 最常用的SQLAlchemy查询执行函数

方法 说明
all() 以列表的形式返回查询的所有结果
fitst() 返回查询的第一个结果,如果没有,则返回None
first_or_404() 返回查询的第一个结果,如果没有,则终止请求,返回404错误响应
get() 返回指定主键对应的行,如果没有,则返回None
get_or_404() 返回指定主键对应的行,如果没有,则返回None
count() 返回查询结果的数量
paginate() 返回一个Paginate对象,它包含指定范围的结果

再看一个从关系的两端查询Role和User之间一对多的关系的例子:

1
2
3
4
5
>>> users = role_visitor.users
>>> users
[<User 'john'>, <User 'david'>]
>>> users[0].role
>>> <Role 'visitor'>

执行role_visitor.users时,隐含的查询会调用all()方法返回一个列表,query对象时隐藏的,因此很难做更进一步的查询。此时可在db.relationship()中添加lazy='dynamic参数,从而禁止自动执行查询(添加后需调用all()等方法才能执行查询,当然也可在执行前调用过滤器)。

5.9 在视图函数中操作数据库

新版的hello.py当输入新用户名时,会把新用户名写入数据库,并显示Pleased to meet you!信息;当输入的是旧用户名(数据库中已有的名字)时,会显示Happy to see you again!

hello.py如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# ...
@app.route('/',methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
session['name'] = form.name.data
# 查找数据库中是否有该用户名,如果没有,就向数据库中添加新用户,
# 因为前面已经设置了SQLALCHEMY_COMMIT_ON_TEARDOWN,所以当请求结束时会自动提交事务
user = User.query.filter_by(user_name=form.name.data).first()
if user is None:
# 插入数据库
user = User(user_name=form.name.data)
db.session.add(user)
# 用于模板中判断是显示Pleased to meet you 还是 Happy to see you again
session['known'] = Flase
else:
session['konwn'] = True
return redirect(url_for('index')
return render_template('index.html', form=form, name=session.get('name'), known=session.get('known', Flase))

index.html如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{% extends "base.html" %}
{% import "bootstarp/wtf.html" as wtf %}
{% block title %}Flasky{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1>
{% if known %}
<p>Happy to see you again!</p>
{% else %}
<p>Pleased to meet you!</p>
{% endif %}
</div>
{{ wtf.quick_form(form) }}
{% endblock %}

5.10 集成Python shell

每次启动shell会话都要将数据库模型和实例一个一个地import进去很麻烦,对此我们可以为shell命令注册一个make_context回调函数,把想import的对象导入列表。
hello.py修改如下:

1
2
3
4
5
6
7
8
9
from flask-script import Shell, Manager
# ...
app = Flask(__name__)
manager = Manager(app)
def make_shell_context():
return dict(app=app, db=db, Role=Role, User=User)
manager.add_command('shell', Shell(make_context=make_shell_context))

这样启动shell时就会将对象直接导入shell中:

1
2
3
4
5
>>> python hello.py shell
>>> app
<Flask 'app>
>>> User
<class 'app.User'>

5.11 使用Flask-Migrate实现数据库迁移

在5.8.1 创建数据库表中我们说到,更新表的方法之一是删除旧表再重新创建表,但是这会丢失原有的数据。现在介绍方法二:使用数据库迁移框架。
数据库迁移框架:能够跟踪数据库模式的变化,然后增量式地把变化应用到已有数据库中。(其功能类似与Git,能够跟踪数据库模式的变化)

可从理解Git的角度理解数据库迁移框架。

5.11.1 创建迁移仓库

hello.py如下:

1
2
3
4
5
from flask-migrate import Migrate, MigrateCommand
# ...
migrate = Migrate(app, db)
manager.add_command('db', MigrateCommand)

Flask-Migrate提供了一个MigrateCommand类,它可以附加到Flask-Script的manager对象上,从而导出数据库迁移命令。该例中,MigrateCommand类使用db命令附加(类似于5.10中,将Shell类使用shell命令附加)。

注意:在维护数据库迁移之前,首先要使用init子命令创建迁移仓库

1
2
3
4
5
(venv)$ python hello.py db init
Creating directory /home/flask/.....
...
...
Please edit configuration/connection/logging setting in '/home/flask/..../alembic.ini' before proceeding.

这个命令会创建一个migrations的文件夹,所有迁移脚本都在里面。

5.11.2 创建迁移脚本

在Alembic中,数据库迁移用迁移脚本表示。脚本中有两个函数:
upgrade()函数:把迁移中的改动应用到数据库中。
downgrade()函数:将改动删除。

我们可以用revision命令手动创建Alembic迁移(upgrade()downgrade()都是空的,需使用Alembic提供的Operations对象指令实现具体操作),
也可以用migrate命令自动创建Alembic迁移(会根据模型定义数据库当前的状态之前的差异生成upgrade()downgrade()函数的内容)。

使用migrate命令自动创建迁移脚本:

1
2
3
4
5
6
(venv)$ python hello.py db migrate -m "initial migration"
INFO [alembic.migration] Context impl SQLiteImpl
...
INFO [alembic.qutogenerate] Detected added table 'users'
...
.../falsky/migrations/versions/1bc...bb5_initial_migration.py...done

5.11.3 更新数据库

检查并修正好迁移脚本后,可以使用db upgrade命令把迁移应用到数据库中。如:

1
2
3
4
(venv)$ python hello.py db upgrade
INFO [alembic.migration] Context impl SQLiteImpl
...
INFO [alembic.migration] Running upgrade None -> 1bc...bb5, initial migration

对于第一个迁移来说,其作用和调用db.create_all()方法一样,但在后续的迁移中,upgrade命令能把改动应用到数据库中,而且不影响其中保存的数据。