数据库按照一定的规则保存程序数据,程序再发起查询取回所需的数据。Web程序最常用基于关系模型的数据库,这种数据库也称为SQL数据库,因为它们使用结构化查询语言。不过最近几年文档数据库和键值对数据库成了流行的替代选择,这两种数据库合称NoSQL数据库。
5.1 SQL数据库
关系型数据库把数据存储在表中,表模拟程序中不同的实体。例如,订单管理程序的数据库中可能有表customers、products和orders。
表的列数是固定的,行数是可变的。列定义表所表示的实体的数据属性。例如,customers表中可能有name、address、phone等列。表中的行定义各列对应的真实数据。
表中有一个特殊的列,称为主键,其值为表中各行的唯一标识符。表中还可以有称为外键的列,引用同一个表或不同表中某行的主键。行之间的这种联系称为关系,这是关系型数据库模型的基础。下图显示了一个简单数据库的关系图。这个数据库有两个表,分别存储用户信息和角色信息。连接两个表的线代表两个表之间的联系。
图1关系型数据库示例
在这个数据库关系图中,roles表存储所有可用的用户角色,每个角色都使用一个唯一的id值(即表的主键)进行标识。users表包含用户列表,每个用户也有唯一的id值。除了id主键之外,roles表中还有name列,users表中还有username列和password列。users表中的role_id列是外键,引用角色的id,通过这种方式为每个用户指定角色。
从这个例子可以看出,关系型数据库存储数据很高效,而且避免了重复。将这个数据库中的用户角色重命名也很简单,因为角色名只能出现在一个地方。一旦roles表中修改完角色名,所有通过role_id引用这个角色的用户都能立刻看到更新。
但从另一方面来说,把数据分别存在多个表中还是很复杂的。生成一个包含角色的用户列表会遇到一个小问题,因为在此之前要分别从两个表中读取用户和用户角色,再将其联结起来。关系型数据库引擎为联结操作提供了必要的支持。
5.2 NoSQL数据库
所有不遵循上节所述的关系模型的数据库统称为NoSQL数据库。NoSQL数据库一般使用集合替代表,使用文档替代记录。NoSQL数据库采用的设计方式使联结变得困难,所以大多数数据库根本不支持这种操作。对于结构如同上面的图的NoSQL数据库,若要列出各用户与角色,就需要在程序中执行联结操作,即先读取每个用户的role_id,再在roles表中搜索对应的记录。
NoSQL更适合设计成图2所示的结构。这是执行反规范化操作得到的结果,它减少了表的数量,却增加了数据重复量。
图2 NoSQL数据库示例
这种结构的数据库要把角色名存储在每个用户中。如此一来,将角色重命名的操作就变得很耗时,可能需要更新大量文档。
使用NoSQL数据库当然也有好处。数据重复可以提升查询速度,列出用户及其角色的操作很简单,因为无需联结。
5.3 SQL or NoSQL?
SQL数据库擅于用高效且紧凑的形式存储结构化数据。这种数据库需要花费大量精力保证数据结构的一致性。NoSQL数据库放宽了对这种一致性的要求,从而获得性能上的优势。
对不同的类型的数据库进行全面分析,超出了本书的范围。不过,对中小型程序来说,SQL和NoSQL数据库都是很好的选择,并且性能相当。
5.4 Python数据库框架
大多数的数据库引擎都由对应的Python包,包括开源包和商业包。Flask并不限制你使用何种类型的数据库包,因此可以根据自己的喜好选择使用MySQL、Postgres、SQLite、Redis 、MongoDB或者CouchDB。
如果这些都无法满足需求,还有一些数据库抽象层代码包供选择,例如SQLAlchemy和MongoEngine。你可以使用这些抽象包直接处理高等级的Python对象,而不是处理如表、文档或查询语言这样的数据库实体。
选择数据库框架时,需要考虑很多因素。
- 易用性:如果直接比较数据库引擎和数据库抽象层,显然后者取胜。抽象层,也称为对象关系映射(Object-Relational Mapper,ORM)或对象文档映射(Object-Document Mapper,ODM),在用户不知觉的情况下把高层的面向对象操作转换成低层的数据库指令。
- 性能:ORM和ODM把对象业务转换成数据库业务会有一定的损耗。大多数情况下,这种性能的降低微不足道,但也不一定都是如此。一般情况下,ORM和ODM对生产率的提升远远超过了这些微的性能降低,所以性能降低这个理由并不足以说服用户完全放弃ORM和ODM。真正的关键点在于如何选择一个能直接操作低层数据库的抽象层,以防特定的操作需要直接使用数据库原生指令优化。
- 可移植性:选择数据库时,必须考虑其是否能在你的开发平台和生产平台中使用。例如,如果你打算利用云平台托管程序,就要知道这个云服务提供了哪些数据库可供选择。可移植性还针对ORM和ODM。尽管有些框架只为一种数据库引擎提供抽象层,但其他框架可能做了更高层的抽象,它们支持不同的数据库引擎,而且都使用相同的面向对象接口。SQLAlchemy ORM就是一个很好的例子,它支持很多关系型数据库引擎包,包括流行的MySQL、Postgres和SQLite。
- Flask集成度:选择框架时,你不一定非得选择已经集成了Flask的框架,但选择这些框架可以节省你编写代码的时间。使用集成了Flask的框架可以简化配置和操作,所以专门为Flask开发的扩展是你的首选。
5.5 使用Flask-SQLAlchemy管理数据库
from flask_sqlalchemy import SQLAlchemy
import os
app = Flask(__name__)
#数据库配置
basedir = os.path.abspath(os.path.dirname(__file__))
app.config['SQLALCHEMY_DATABASE_URI'] = \
'sqlite:///'+os.path.join(basedir,'data.sqlite')
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = Flase
#数据库实例化
db = SQLAlchemy(app)
db对象是SQLAlchemy类的实例,表示程序使用的数据库,同时还获得了Flask-SQLAlchemy提供的所有功能。
5.6 定义模型
#定义数据库的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,unique=True,index=True)
def __repr__(self):
return '<User %s>' %self.username
类变量__tablename__定义在数据库中使用的表名。如果没有定义__tablename__,Flask-SQLAlchemy会使用一个默认名字,
但默认的表名没有遵守使用复数形式进行命名的约定,所以最好我们自己来指定一个复数形式的表名。其余的类变量都是该模型的属性,被定义为db.Column类的实例。
5.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'))
如图1所示,关系使用users表中的外键连接了两行。添加到User模型中的role_id列被定义为外键,就是这个外键建立起了关系。传给db.ForeignKey()的参数‘’roles.id‘’表明,这列的值就是roles表中的id值。
除了一对多之外,还有几种其他的类型关系。一对一关系可以用前面介绍的一对多关系表示,但调用db.relationship()时要把uselist设为False,把“多”变成“一”。多对一关系也可使用一对多表示,对调两个表即可,或者把外键和db.relationship()都放在“多”这一侧。最复杂的关系的多对多,需要用到第三张表来存放多对多关系,这个表称为关系表。
5.8 数据库操作
现在模型已经按照图1所示的数据库关系图完成配置,可以随时使用了。学习如何使用模型的最好方法是在Python shell中实际操作(使用PyCharm也可以在其Python Shell中进行操作,此时的的运行环境会默认在当前目录下,这省去了我们导入自定义模块的环境变量配置的麻烦)。下面的几节将介绍最常用的数据库操作。5.8.1 创建表
首先,我们要让Flask-SQLAlchemy根据模型类创建数据库。方法是使用db.create_all()函数:
此时我们会看到,一个名为data.sqlite 的文件在当前目录下被建立。
这个SQLite数据库文件的名字就是在配置中指定的。如果数据库表已经存在于数据库中,那么db.create_all()不会重新创建或者更新这个表。如果修改模型后要把改动应用到现有的数据库中,这一特性会带来不便,更新现有数据库表的粗暴方式是先删除旧表再重新创建:
但是,这个方法有我们不想看到的副作用,它把数据库原有的数据全都销毁。本章末尾将会介绍一种更好的方式用于更新数据库。
5.8.2 插入行
下面这段代码创建了一些角色和用户:模型的构造函数接受的参数是使用关键字参数指定的模型属性初始值。注意,role属性也可使用,虽然它不是真正的数据库列,但却是一对多关系的高级表示。这些新建对象的id属性并没有被明确设定,因为主键是由Flask-SQLAlchemy管理的。现在这些对象只存在于python中,还未写入数据库,因此id尚未赋值。
通过数据库会话管理对数据库所做的改动,在Flask-SQLAlchemy中,会话由db.session表示。准备把对象写入数据库之前,先要将其添加到对话中:
db.session.add(admin_role)
#......
#或者
db.session.add_all([admin_role,mod_role,user_role,user_john,user_susan,user_david])
现在,为了把对象写入数据库,我们需要调用commit()方法提交会话:
db.session.commit()
这时,再次查看id属性,现在它们已经赋值了。
数据库会话db.session和之前介绍的FlaskSession对象没有关系。数据库会话也称为事务。
数据库回话能保证数据库的一致性。提交操作使用原子方式把会话中的全部对象写入数据库。如果在写入的过程中发生了错误,整个会话都会失效。如果你始终把相关改动放
在会话中提交,就能避免因部分更新导致的数据库不一致性。
数据库会话也可回滚。调用 db.session.rollback() 后,添加到数据库会话中的所有对象都会还原到它们在数据库时的状态。
5.8.3 修改行
在数据库会话上调用 add() 方法也能更新模型。我们继续在之前的 shell 会话中进行操作,下面这个例子把 "Admin" 角色重命名为 "Administrator":
>>> admin_role.name = 'Administrator'
>>> db.session.add(admin_role)
>>> db.session.commit()
5.8.4 删除行
数据库会话还有个 delete() 方法。下面这个例子把 "Moderator" 角色从数据库中删除:
>>> db.session.delete(mod_role)
>>> db.session.commit()
注意,删除与插入和更新一样,提交数据库会话后才会执行。
5.8.5 查询行
Flask-SQLAlchemy 为每个模型类都提供了 query 对象。
最基本的模型查询是取回对应表中的所有记录:
>>> Role.query.all()
[<Role u'Administrator'>, <Role u'User'>]
>>> User.query.all()
[<User u'john'>, <User u'susan'>, <User u'david'>]
使用过滤器可以配置 query 对象进行更精确的数据库查询。下面这个例子查找角色为"User" 的所有用户:
>>> User.query.filter_by(role=user_role).all()
[<User u'susan'>, <User u'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()
filter_by() 等过滤器在 query 对象上调用,返回一个更精确的 query 对象。多个过滤器可以一起调用,直到获得所需结果。
下表列出了可在 query 对象上调用的常用过滤器。完整的列表参见 SQLAlchemy 文档(http://docs.sqlalchemy.org)。
在查询上应用指定的过滤器后,通过调用 all() 执行查询,以列表的形式返回结果。除了all() 之外,还有其他方法能触发查询执行。下表列出了执行查询的其他方法。
关系和查询的处理方式类似。下面这个例子分别从关系的两端查询角色和用户之间的一对多关系:
这个例子的user_role.users查询有个小问题。执行user_role.users表达式时,隐含的查询会调用all()返回一个用户列表。query对象是隐藏的,因此无法指定更精确的查询过滤器。就这个特定示例而言,返回一个按照字母顺序排序的用户列表可能会更好。在下例中,我们修改了关系的设置,加入了lazy = ‘dynamic’参数,从而禁止自动执行查询。
示例 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 u'david'>, <User u'susan'>]
>>> user_role.users.count()
2
5.9 在视图函数中操作数据库
前一节介绍的数据库操作可以直接在视图函数中进行。下面示例展示了首页路由的新版本,已经把用户输入的名字写入了数据库。
示例 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
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 中创建数据库表。
对应的模板新版本如示例所示。这个模板使用known 参数在欢迎消息中加入了第二行,从而对已知用户和新用户显示不同的内容。
示例 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 %}
5.10 集成Python shell
每次启动shell 会话都要导入数据库实例和模型,这真是份枯燥的工作。为了避免一直重复导入,我们可以做些配置,让Flask-Script 的shell 命令自动导入特定的对象。
若想把对象添加到导入列表中,我们要为shell 命令注册一个make_context 回调函数,如示例所示。
示例 hello.py:为shell命令添加一个上下文
from flask_script import Shell,Manager
manager = Manager(app)
def make_shell_context():
return dict(app=app, db=db, User=User, Role=Role)
manager.add_command("shell", Shell(make_context=make_shell_context))
make_shell_context() 函数注册了程序、数据库实例以及模型,因此这些对象能直接导入shell:
$ python hello.py shell
>>> app
<Flask 'app'>
>>> db
<SQLAlchemy engine='sqlite:home/flask/flasky/data.sqlite'>
>>> User
<class 'app.User'>
5.11 使用Flask-Migrate实现数据库迁移
后续涉及到了再行补充。