配套视频教程
使用Flask-SQLAlchemy管理数据库
Flask-SQLAlchemy 是一个 Flask 扩展,简化了在 Flask 应用中使用 SQLAlchemy 的操作。SQLAlchemy 是一个强大的关系型数据库框架,支持多种数据库后台。SQLAlchemy 提供了高层 ORM,也提供了使用数据库原生 SQL 的低层功能。
与其他多数扩展一样,Flask-SQLAlchemy 也使用 pip 安装:
(venv) $ pip install flask-sqlalchemy
在 Flask-SQLAlchemy 中,数据库使用 URL 指定。几种最流行的数据库引擎使用的 URL 格式如表 1 所示。
表1:FLask-SQLAlchemy数据库URL
数据库引擎 | URL |
---|---|
MySQL | mysql://username:password@hostname/database |
Postgres | postgresql://username:password@hostname/database |
SQLite(Linux,macOS) | sqlite:////absolute/path/to/database |
SQLite(Windows) | sqlite:///c:/absolute/path/to/database |
在这些 URL 中,hostname 表示数据库服务所在的主机,可以是本地主机(localhost),也可以是远程服务器。数据库服务器上可以托管多个数据库,因此 database 表示要使用的数据库名。如果数据库需要验证身份,使用 username 和 password 提供数据库用户的凭据。
SQLite 数据库没有服务器,因此不用指定 hostname、username 和 password。URL 中的 database 是磁盘中的文件名。
应用使用的数据库 URL 必须保存到 Flask 配置对象的 SQLALCHEMY_DATABASE_URI
键中。Flask-SQLAlchemy 文档还建议把 SQLALCHEMY_TRACK_MODIFICATIONS
键设为 False
,以便在不需要跟踪对象变化时降低内存消耗。其他配置选项的作用参阅 Flask-SQLAlchemy 的文档。示例 1 展示如何初始化及配置一个简单的 SQLite 数据库。
示例 1 hello.py:配置数据库
import os
from flask_sqlalchemy import SQLAlchemy
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_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
db
对象是 SQLAlchemy
类的实例,表示应用使用的数据库,通过它可获得 Flask-SQLAlchemy 提供的所有功能。
定义模型
模型这个术语表示应用使用的持久化实体。在 ORM 中,模型一般是一个 Python 类,类中的属性对应于数据库表中的列。
Flask-SQLAlchemy 创建的数据库实例为模型提供了一个基类以及一系列辅助类和辅助函数,可用于定义模型的结构。图中的 roles
表和 users
表可像示例 2 那样,定义为 Role
和 User
模型。
示例 2 hello.py:定义
Role
和User
模型
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
def __repr__(self):
return '<Role %r>' % self.name
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
def __repr__(self):
return '<User %r>' % self.username
类变量 __tablename__
定义在数据库中使用的表名。如果没有定义 __tablename__
,Flask-SQLAlchemy 会使用一个默认名称,但默认的表名没有遵守流行的使用复数命名的约定,所以最好由我们自己来指定表名。其余的类变量都是该模型的属性,定义为 db.Column
类的实例。
db.Column
类构造函数的第一个参数是数据库列和模型属性的类型。表 2 列出了一些可用的列类型以及在模型中使用的 Python 类型。
表2:最常用的SQLAlchemy列类型
类型名 | Python类型 | 说明 |
---|---|---|
Integer |
int |
普通整数,通常是 32 位 |
SmallInteger |
int |
取值范围小的整数,通常是 16 位 |
BigInteger |
int 或 long |
不限制精度的整数 |
Float |
float |
浮点数 |
Numeric |
decimal.Decimal |
定点数 |
String |
str |
变长字符串 |
Text |
str |
变长字符串,对较长或不限长度的字符串做了优化 |
Unicode |
unicode |
变长 Unicode 字符串 |
UnicodeText |
unicode |
变长 Unicode 字符串,对较长或不限长度的字符串做了优化 |
Boolean |
bool |
布尔值 |
Date |
datetime.date |
日期 |
Time |
datetime.time |
时间 |
DateTime |
datetime.datetime |
日期和时间 |
Interval |
datetime.timedelta |
时间间隔 |
Enum |
str |
一组字符串 |
PickleType |
任何 Python 对象 | 自动使用 Pickle 序列化 |
LargeBinary |
str |
二进制 blob |
db.Column
的其余参数指定属性的配置选项。表 3 列出了一些可用选项。
表3:最常用的SQLAlchemy列选项
选项名 | 说明 |
---|---|
primary_key |
如果设为 True ,列为表的主键 |
unique |
如果设为 True ,列不允许出现重复的值 |
index |
如果设为 True ,为列创建索引,提升查询效率 |
nullable |
如果设为 True ,列允许使用空值;如果设为 False ,列不允许使用空值 |
default |
为列定义默认值 |
Flask-SQLAlchemy 要求每个模型都定义主键,这一列经常命名为
id
。
虽然没有强制要求,但这两个模型都定义了 __repr()__
方法,返回一个具有可读性的字符串表示模型,供调试和测试时使用。
关系
关系型数据库使用关系把不同表中的行联系起来。上图所示的关系图表示用户和角色之间的一种简单关系。这是角色到用户的一对多关系,因为一个角色可属于多个用户,而每个用户都只能有一个角色。
图中的一对多关系在模型类中的表示方法如示例3 所示。
示例3 hello.py:在数据库模型中定义关系
class Role(db.Model):
# ...
users = db.relationship('User', backref='role')
class User(db.Model):
# ...
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
如上图所示,关系使用 users
表中的外键连接两行。添加到 User
模型中的 role_id
列被定义为外键,就是这个外键建立起了关系。传给 db.ForeignKey()
的参数 'roles.id'
表明,这列的值是 roles
表中相应行的 id
值。
从“一”那一端可见,添加到 Role
模型中的 users
属性代表这个关系的面向对象视角。对于一个 Role
类的实例,其 users
属性将返回与角色相关联的用户组成的列表(即“多”那一端)。db.relationship()
的第一个参数表明这个关系的另一端是哪个模型。如果关联的模型类在模块后面定义,可使用字符串形式指定。
db.relationship()
中的 backref
参数向 User
模型中添加一个 role
属性,从而定义反向关系。通过 User
实例的这个属性可以获取对应的 Role
模型对象,而不用再通过 role_id
外键获取。
多数情况下,db.relationship()
都能自行找到关系中的外键,但有时却无法确定哪一列是外键。例如,如果 User 模型中有两个或以上的列定义为 Role
模型的外键,SQLAlchemy 就不知道该使用哪一列。如果无法确定外键,就要为 db.relationship()
提供额外的参数。表 4 列出了定义关系时常用的配置选项。
表4:常用的SQLAlchemy关系选项
选项名 | 说明 |
---|---|
backref |
在关系的另一个模型中添加反向引用 |
primaryjoin |
明确指定两个模型之间使用的联结条件;只在模棱两可的关系中需要指定 |
lazy |
指定如何加载相关记录,可选值有 select (首次访问时按需加载)、immediate (源对象加载后就加载)、joined (加载记录,但使用联结)、subquery (立即加载,但使用子查询),noload (永不加载)和 dynamic (不加载记录,但提供加载记录的查询) |
uselist |
如果设为 False ,不使用列表,而使用标量值 |
order_by |
指定关系中记录的排序方式 |
secondary |
指定多对多关系中关联表的名称 |
secondaryjoin |
SQLAlchemy 无法自行决定时,指定多对多关系中的二级联结条件 |
数据库操作
现在模型已经按照上图所示的数据库关系图完成配置,可以随时使用了。学习使用模型的最好方法是在 Python shell 中实际操作。接下来的几节将介绍最常用的数据库操作。shell 使用 flask shell
命令启动。不过在执行这个命令之前,要把 FLASK_APP
环境变量设为 hello.py
。
创建表
首先,要让 Flask-SQLAlchemy 根据模型类创建数据库。db.create_all()
函数将寻找所有 db.Model
的子类,然后在数据库中创建对应的表:
(venv) $ flask shell
>>> from hello import db
>>> db.create_all()
现在查看应用目录,你会发现有个名为 data.sqlite 的文件,文件名与配置中指定的一样。如果数据库表已经存在于数据库中,那么 db.create_all()
不会重新创建或者更新相应的表。如果修改模型后要把改动应用到现有的数据库中,这一行为会带来不便。更新现有数据库表的蛮力方式是先删除旧表再重新创建:
>>> db.drop_all()
>>> db.create_all()
遗憾的是,这个方法有个我们不想看到的副作用,它把数据库中原有的数据都销毁了。本章末尾将介绍一种更好的数据库更新方式。
插入行
下面这段代码创建一些角色和用户:
>>> from hello import Role, User
>>> admin_role = Role(name='Admin')
>>> mod_role = Role(name='Moderator')
>>> user_role = Role(name='User')
>>> user_john = User(username='john', role=admin_role)
>>> user_susan = User(username='susan', role=user_role)
>>> user_david = User(username='david', role=user_role)
模型的构造函数接受的参数是使用关键字参数指定的模型属性初始值。注意,role
属性也可使用,虽然它不是真正的数据库列,但却是一对多关系的高级表示。新建对象时没有明确设定 id
属性,因为在多数数据库中主键由数据库自身管理。现在这些对象只存在于 Python 中,还未写入数据库。因此,id
尚未赋值:
>>> print(admin_role.id)
None
>>> print(mod_role.id)
None
>>> print(user_role.id)
None
对数据库的改动通过数据库会话管理,在 Flask-SQLAlchemy 中,会话由 db.session
表示。准备把对象写入数据库之前,要先将其添加到会话中:
>>> db.session.add(admin_role)
>>> db.session.add(mod_role)
>>> db.session.add(user_role)
>>> db.session.add(user_john)
>>> db.session.add(user_susan)
>>> db.session.add(user_david)
或者简写成:
>>> db.session.add_all([admin_role, mod_role, user_role,
... user_john, user_susan, user_david])
为了把对象写入数据库,我们要调用 commit()
方法提交会话:
>>> db.session.commit()
提交数据后再查看 id
属性,现在它们已经赋值了:
>>> print(admin_role.id)
1
>>> print(mod_role.id)
2
>>> print(user_role.id)
3
数据库会话能保证数据库的一致性。提交操作使用原子方式把会话中的对象全部写入数据库。如果在写入会话的过程中发生了错误,那么整个会话都会失效。如果你始终把相关改动放在会话中提交,就能避免因部分更新导致的数据库不一致。
修改行
在数据库会话上调用 add()
方法也能更新模型。我们继续在之前的 shell 会话中进行操作,下面这个例子把 "Admin"
角色重命名为 "Administrator"
:
>>> admin_role.name = 'Administrator'
>>> db.session.add(admin_role)
>>> db.session.commit()
删除行
数据库会话还有个 delete()
方法。下面这个例子把 "Moderator"
角色从数据库中删除:
>>> db.session.delete(mod_role)
>>> db.session.commit()
注意,删除与插入和更新一样,提交数据库会话后才会执行。
查询行
Flask-SQLAlchemy 为每个模型类都提供了 query
对象。最基本的模型查询是使用 all()
方法取回对应表中的所有记录:
>>> Role.query.all()
[<Role 'Administrator'>, <Role 'User'>]
>>> User.query.all()
[<User 'john'>, <User 'susan'>, <User 'david'>]
使用过滤器可以配置 query
对象进行更精确的数据库查询。下面这个例子查找角色为 "User"
的所有用户:
>>> User.query.filter_by(role=user_role).all()
[<User 'susan'>, <User 'david'>]
若想查看 SQLAlchemy 为查询生成的原生 SQL 查询语句,只需把 query
对象转换成字符串:
>>> str(User.query.filter_by(role=user_role))
'SELECT users.id AS users_id, users.username AS users_username,
users.role_id AS users_role_id
FROM users
WHERE :param_1 = users.role_id'
如果你退出了 shell 会话,前面这些例子中创建的对象就不会以 Python 对象的形式存在,但在数据库表中仍有对应的行。如果打开一个新的 shell 会话,要从数据库中读取行,重新创建 Python 对象。下面这个例子发起一个查询,加载名为 "User"
的用户角色:
>>> user_role = Role.query.filter_by(name='User').first()
注意,这里发起查询的不是 all()
方法,而是 first()
方法。all()
方法返回所有结果构成的列表,而 first()
方法只返回第一个结果,如果没有结果的话,则返回 None
。因此,如果知道查询最多返回一个结果,就可以用这个方法。
filter_by()
等过滤器在 query
对象上调用,返回一个更精确的 query
对象。多个过滤器可以一起调用,直到获得所需结果。
表 5 列出了可在 query
对象上调用的常用过滤器。完整的列表参见 SQLAlchemy 文档(http://docs.sqlalchemy.org)。
表5:常用的SQLAlchemy查询过滤器
过滤器 | 说明 |
---|---|
filter() |
把过滤器添加到原查询上,返回一个新查询 |
filter_by() |
把等值过滤器添加到原查询上,返回一个新查询 |
limit() |
使用指定的值限制原查询返回的结果数量,返回一个新查询 |
offset() |
偏移原查询返回的结果,返回一个新查询 |
order_by() |
根据指定条件对原查询结果进行排序,返回一个新查询 |
group_by() |
根据指定条件对原查询结果进行分组,返回一个新查询 |
在查询上应用指定的过滤器后,调用 all()
方法将执行查询,以列表的形式返回结果。除了 all()
方法之外,还有其他方法能触发查询执行。表6 列出了执行查询的其他方法。
表6:最常用的SQLAlchemy查询执行方法
方法 | 说明 |
---|---|
all() |
以列表形式返回查询的所有结果 |
first() |
返回查询的第一个结果,如果没有结果,则返回 None |
first_or_404() |
返回查询的第一个结果,如果没有结果,则终止请求,返回 404 错误响应 |
get() |
返回指定主键对应的行,如果没有对应的行,则返回 None |
get_or_404() |
返回指定主键对应的行,如果没找到指定的主键,则终止请求,返回 404 错误响应 |
count() |
返回查询结果的数量 |
paginate() |
返回一个 Paginate 对象,包含指定范围内的结果 |
关系与查询的处理方式类似。下面这个例子分别从关系的两端查询角色和用户之间的一对多关系:
>>> users = user_role.users
>>> users
[<User 'susan'>, <User 'david'>]
>>> users[0].role
<Role 'User'>
这个例子中的 user_role.users
查询有个小问题。执行 user_role.users
表达式时,隐式的查询会调用 all()
方法,返回一个用户列表。此时,query
对象是隐藏的,无法指定更精确的查询过滤器。就这个示例而言,返回一个按照字母顺序排列的用户列表可能更好。在示例4 中,我们修改了关系的设置,加入了 lazy='dynamic'
参数,从而禁止自动执行查询。
示例4 hello.py:动态数据库关系
class Role(db.Model):
# ...
users = db.relationship('User', backref='role', lazy='dynamic')
# ...
这样配置关系之后,user_role.users
将返回一个尚未执行的查询,因此可以在其上添加过滤器:
>>> user_role.users.order_by(User.username).all()
[<User 'david'>, <User 'susan'>]
>>> user_role.users.count()
2
在视图函数中操作数据库
前一节介绍的数据库操作可以直接在视图函数中进行。示例 5 是首页路由的新版本,把用户输入的名字记录到数据库中。
示例 5 hello.py:在视图函数中操作数据库
@app.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.name.data).first()
if user is None:
user = User(username=form.name.data)
db.session.add(user)
db.session.commit()
session['known'] = False
else:
session['known'] = True
session['name'] = form.name.data
form.name.data = ''
return redirect(url_for('index'))
return render_template('index.html',
form=form, name=session.get('name'),
known=session.get('known', False))
在这个修改后的版本中,提交表单后,应用会使用 filter_by()
查询过滤器在数据库中查找提交的名字。变量 known
被写入用户会话中,因此重定向之后,可以把数据传给模板,用于显示自定义的欢迎消息。注意,为了让应用正常运行,必须按照前面介绍的方法,在 Python shell 中创建数据库表。
对应的模板新版本如示例 6 所示。这个模板使用 known
参数在欢迎消息中加入了第二行,从而对已知用户和新用户显示不同的内容。
示例 6 templates/index.html:在模板中定制欢迎消息
{% extends "base.html" %}
{% import "bootstrap/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 not known %}
<p>Pleased to meet you!</p>
{% else %}
<p>Happy to see you again!</p>
{% endif %}
</div>
{{ wtf.quick_form(form) }}
{% endblock %}
集成Python shell
每次启动 shell 会话都要导入数据库实例和模型,这真是份枯燥的工作。为了避免一直重复导入,我们可以做些配置,让 flask shell
命令自动导入这些对象。
若想把对象添加到导入列表中,必须使用 app.shell_context_processor
装饰器创建并注册一个 shell 上下文处理器,如示例7 所示。
示例 7 hello.py:添加一个 shell 上下文
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
这个 shell 上下文处理器函数返回一个字典,包含数据库实例和模型。除了默认导入的 app
之外,flask shell
命令将自动把这些对象导入 shell。
$ flask shell
>>> app
<Flask 'hello'>
>>> db
<SQLAlchemy engine='sqlite:////home/flask/flasky/data.sqlite'>
>>> User
<class 'hello.User'>
使用Flask-Migrate实现数据库迁移
在开发应用的过程中,你会发现有时需要修改数据库模型,而且修改之后还要更新数据库。仅当数据库表不存在时,Flask-SQLAlchemy 才会根据模型创建。因此,更新表的唯一方式就是先删除旧表,但是这样做会丢失数据库中的全部数据。
更新表更好的方法是使用数据库迁移框架。源码版本控制工具可以跟踪源码文件的变化;类似地,数据库迁移框架能跟踪数据库模式的变化,然后以增量的方式把变化应用到数据库中。
SQLAlchemy 的开发人员编写了一个迁移框架,名为 Alembic。除了直接使用 Alembic 之外,Flask 应用还可使用 Flask-Migrate 扩展。这个扩展是对 Alembic 的轻量级包装,并与 flask
命令做了集成。
创建迁移仓库
首先,要在虚拟环境中安装 Flask-Migrate:
(venv) $ pip install flask-migrate
这个扩展的初始化方法如示例 8 所示。
示例 8 hello.py:初始化 Flask-Migrate
from flask_migrate import Migrate
# ...
migrate = Migrate(app, db)
为了开放数据库迁移相关的命令,Flask-Migrate 添加了 flask db
命令和几个子命令。在新项目中可以使用 init
子命令添加数据库迁移支持:
(venv) $ flask db init
Creating directory /home/flask/flasky/migrations...done
Creating directory /home/flask/flasky/migrations/versions...done
Generating /home/flask/flasky/migrations/alembic.ini...done
Generating /home/flask/flasky/migrations/env.py...done
Generating /home/flask/flasky/migrations/env.pyc...done
Generating /home/flask/flasky/migrations/README...done
Generating /home/flask/flasky/migrations/script.py.mako...done
Please edit configuration/connection/logging settings in
'/home/flask/flasky/migrations/alembic.ini' before proceeding.
这个命令会创建 migrations 目录,所有迁移脚本都存放在这里。
创建迁移脚本
在 Alembic 中,数据库迁移用迁移脚本表示。脚本中有两个函数,分别是 upgrade()
和 downgrade()
。upgrade()
函数把迁移中的改动应用到数据库中,downgrade()
函数则将改动删除。Alembic 具有添加和删除改动的能力,意味着数据库可重设到修改历史的任意一点。
使用 Flask-Migrate 管理数据库模式变化的步骤如下。
(1) 对模型类做必要的修改。
(2) 执行 flask db migrate
命令,自动创建一个迁移脚本。
(3) 检查自动生成的脚本,根据对模型的实际改动进行调整。
(4) 把迁移脚本纳入版本控制。
(5) 执行 flask db upgrade
命令,把迁移应用到数据库中。
flask db migrate
子命令用于自动创建迁移脚本:
(venv) $ flask db migrate -m "initial migration"
INFO [alembic.migration] Context impl SQLiteImpl.
INFO [alembic.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate] Detected added table 'roles'
INFO [alembic.autogenerate] Detected added table 'users'
INFO [alembic.autogenerate.compare] Detected added index
'ix_users_username' on '['username']'
Generating /home/flask/flasky/migrations/versions/1bc
594146bb5_initial_migration.py...done
更新数据库
检查并修正好迁移脚本之后,执行 flask db upgrade
命令,把迁移应用到数据库中:
(venv) $ flask db upgrade
INFO [alembic.migration] Context impl SQLiteImpl.
INFO [alembic.migration] Will assume non-transactional DDL.
INFO [alembic.migration] Running upgrade None -> 1bc594146bb5, initial migration
对第一个迁移来说,其作用与调用 db.create_all()
方法一样。但在后续的迁移中,flask db upgrade
命令能把改动应用到数据库中,且不影响其中保存的数据。
如果你按照之前的说明操作过,那么已经使用
db.create_all()
函数创建了数据库文件。此时,flask db upgrade
命令将失败,因为它试图创建已经存在的数据库表。一种简单的处理方法是,把 data.sqlite 数据库文件删掉,然后执行flask db upgrade
命令,通过迁移框架重新创建数据库。