5.1 SQL数据库
关系型数据库把数据存储在表中,表模拟程序中不同的实体。例如,订单管理程序的数据库中可能有表customers、products和orders。
表的列数是固定的,行数是可变的。列定义表所表示的实体的数据属性。例如,customers表中可能有name、address、phone等列。表中的行定义各列对应的真实数据。
主键,其值为表中各行的唯一标识符。外键,引用同一个表或不同表中某行的主键。行之间的这种联系称为关系,这是关系型数据库模型的基础。
图5-1展示了一个简单数据库的关系图。这个数据库中有两个表,分别存储用户和用户角色。连接两个表的线代表两个表之间的关系。
在这个数据库关系图中,roles表存储所有可用的用户角色,每个角色都使用一个唯一的id值(即表的主键)进行标识。users表包含用户列表,每个用户也有唯一的id值。除了id主键之外,roles表中还有name列,users表中还有username列和password列。users表中的role_id列是外键,引用角色的id,通过这种方式为每个用户指定角色。
一旦在roles表中修改完角色,所有通过role_id引用这个角色的用户都能立即看到更新。
5.2 NoSQL数据库
所有不遵循上节所述的关系模型的数据库统称为NoSQL数据库。NoSQL数据库一般使用集合代替表,使用文档代替记录。NoSQL数据库采用的设计方式是联结变得困难,所以大多数数据库根本不支持这种操作。对于结果如图5-1所示的NoSQL数据库,若要列出各用户及其角色,就需要在程序中执行联结操作,即先读取每个用户的role_id,再在roles表中搜索对应的记录。
5.3 使用SQL还是NoSQL
SQL数据库擅于用高效且紧凑的形式存储结构化数据。这种数据库需要花费大量精力保证数据的一致性。NoSQL数据库放宽了对这种一致性的要求,从而获得性能上的优势。
5.4 Python数据库框架
Flask可以根据自己喜好选择使用MySQL、Postgres、SQLite、Redis、MongoDB或者CouchDB。
选择数据库框架时,你要考虑很多因素。
- 易用性
- 性能
- 可移植性
- Flask集成度
5.5 使用Flask-SQLAlchemy管理数据库
Flask-SQLAlchemy是一个Flask扩展,简化了在Flask程序中使用SQLAlchemy的操作。
SQLAlchemy是一个强大的关系型数据库框架,支持多种数据库后台。SQLAlchemy提供了高层ORM,也提供了使用数据库原生SQL的底层功能。
pip install flask-sqlalchemy
程序使用的数据库URL必须保存到Flask配置对象的SQLALCHEMY_DATABASE_URI键中。配置对象中还有一个很有用的选项,即SQLALCHEMY_COMMIT_ON_TEARDOWN键,将其设为True时,每次请求结束后都会自动提交数据库中的变动。其他配置选项的作用参考Flask-SQLAlchemy的文档。Flask-SQLAlchemy文档
示例5-1 hello.py:配置数据库
#coding:utf8
from flask import Flask
from 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)
5.6 定义模型
示例5-2 hello.py:定义Role和User模型
#coding:utf8
from flask import Flask
from 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)
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
5.7 关系
图5-1所示的关系图表示用户和角色之间的一种简单关系。这是角色到用户的一对多关系,因为一个角色可属于多个用户,而每个用户都只能有一个角色。
图5-1中的一对多关系在模型类中的表示方法如示例5-3所示。
示例5-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'))
db.relationship()中的backref参数向User模型中添加一个role属性,从而定义反向关系。这一属性可替代role_id访问Role模型,此时获取的是模型对象,而不是外键的值。
5.8 数据库操作
5.8.1 创建表
在hello.py中添加:
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
最终hello.py:
#coding:utf8
from flask import Flask
from 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
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
db = SQLAlchemy(app)
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer,primary_key=True)
name = db.Column(db.String(64),unique=True)
users = db.relationship('User',backref='role')
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)
role_id = db.Column(db.Integer,db.ForeignKey('roles.id'))
def __repr__(self):
return '<User %r>' % self.username
更新现有数据库表的粗暴方式是先删除旧表在重新创建:
>>> db.drop_all()
>>> db.create_all()
5.8.2 插入行
这些新建对象的id属性并没有明确设定,因为主键是由Flask-SQLAlchemy管理的。现在这些对象只存在于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
5.8.3 修改行
>>> admin_role.name = 'Administrator'
>>> db.session.add(admin_role)
>>> db.session.commit()
5.8.4 删除行
>>> db.session.delete(mod_role)
>>> db.session.commit()
5.8.5 查询行
使用过滤器可以配置query对象进行更精确的数据库查询。下面这个例子查找角色为"User"的所有用户:
若要查看SQLAlchemy为查询生成的原生SQL查询语句,只需把query对象转换成字符串:
如果你退出了shell会话,前面这些例子中创建的对象就不会以Python对象的形式存在,而是作为各自数据库表的行。如果你打开了一个新的shell会话,就要从数据库中读取行,再重新创建Python对象。下面这个例子发起了一个查询,加载名为"User"的用户角色:
>>> user_role = Role.query.filter_by(name='User').first()
关系和查询的处理方式类似。下面这个例子分别从关系的两端查询角色和用户之间的一对多关系:
这个例子中的user_role.users查询有个小问题。执行user_role.users表达式时,隐含的查询会调用all()返回一个用户列表。query对象是隐藏的,因此无法指定更精确的查询过滤器。就这个特定示例而言,返回一个按照字母顺序排序的用户列表可能更好。在示例5-4中,我们修改了关系的设置,加入了lazy='dynamic'参数,从而禁止自动执行查询。
示例5-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 u'david'>, <User u'susan'>]
>>> user_role.users.count()
2
5.9 在视图函数中操作数据库
示例5-5 hello.py:在视图函数中操作数据库
#coding:utf8
from flask import Flask,render_template
from flask_wtf import Form
from flask_bootstrap import Bootstrap
app=Flask(__name__)
bootstrap=Bootstrap(app)
@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)
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))
if __name__ == '__main__':
app.run(debug=True)
提交表单后,程序会使用filter_by()查询过滤器在数据库中查找提交的名字。变量known被写入用户会话中,因此重定向后,可以把数据传给模板,用来显示自定义的欢迎消息。注意,要想让程序正常运行,你必须按照前面介绍的方法,在Python shell中创建数据库表。
示例5-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 %}
5.10 集成Python shell
每次启动shell会话都要导入数据库实例和模型,这真是份枯燥的工作。为了避免一直重复导入,我们可以做些配置,让Flask-Script的shell命令自动导入特定的对象。
若想把对象添加到导入列表中,我们要为shell命令注册一个make_context回调函数,如示例5-7所示。
示例5-7 hello.py:为shell命令添加一个上下文
#coding:utf8
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os
from flask import Flask,render_template
from flask_wtf import Form
from flask_bootstrap import Bootstrap
from flask_script import Shell,Manager
app=Flask(__name__)
bootstrap=Bootstrap(app)
manager=Manager(app)
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
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
db = SQLAlchemy(app)
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer,primary_key=True)
name = db.Column(db.String(64),unique=True)
users = db.relationship('User',backref='role',lazy='dynamic')
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)
role_id = db.Column(db.Integer,db.ForeignKey('roles.id'))
def __repr__(self):
return '<User %r>' % self.username
@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)
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))
def make_shell_context():
return dict(app=app,db=db,User=User,Role=Role)
manager.add_command("shell",Shell(make_context=make_shell_context))
if __name__ == '__main__':
#app.run(debug=True)
manager.run()
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实现数据库迁移
SQLAlchemy的主力开发人员编写了一个迁移框架,称为Alembic(http://alembic.zzzcomputing.com/en/latest/)。除了直接使用Alembic之外,Flask程序还可使用Flask-Migrate(http://flask-migrate.readthedocs.io/en/latest/)扩展。这个扩展对Alembic做了轻量级包装,并集成到Flask-Script中,所有操作都通过Flask-Script命令完成。
5.11.1 创建迁移仓库
pip install flask-migrate
示例5-8 hello.py:配置Flask-Migrate
#coding:utf8
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os
from flask import Flask,render_template
from flask_wtf import Form
from flask_bootstrap import Bootstrap
from flask_script import Shell,Manager
from flask_migrate import Migrate,MigrateCommand
app=Flask(__name__)
bootstrap=Bootstrap(app)
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
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
db = SQLAlchemy(app)
migrate=Migrate(app,db)
manager=Manager(app)
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer,primary_key=True)
name = db.Column(db.String(64),unique=True)
users = db.relationship('User',backref='role',lazy='dynamic')
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)
role_id = db.Column(db.Integer,db.ForeignKey('roles.id'))
def __repr__(self):
return '<User %r>' % self.username
@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)
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))
def make_shell_context():
return dict(app=app,db=db,User=User,Role=Role)
manager.add_command('db',MigrateCommand)
if __name__ == '__main__':
#app.run(debug=True)
manager.run()
这个命令会创建migrations文件夹,所有迁移脚本都存放其中。
5.11.2 创建迁移脚本
在Alembic中,数据库迁移用迁移脚本表示。脚本中有两个函数,分别是upgrade()和downgrade()。upgrade()函数把迁移中的改动应用到数据库中,downgrade()函数则将改动删除。Alembic具有添加和删除改动的能力,因此数据库可重设到修改历史的任意一点。
我们可以使用revision命令手动创建Alembic迁移,也可使用migrate命令自动创建。手动创建的迁移只是一个骨架,upgrade()和downgrade()函数都是空的,开发者要使用Alembic提供的Operations对象指令实现具体操作。自动创建的迁移会根据模型定义和数据库当前状态之间的差异生成upgrade()和downgrade()函数的内容。
migrate子命令用来自动创建迁移脚本:
5.11.3 更新数据库
检查并修正好迁移脚本之后,我们可以使用db upgrade命令把迁移应用到数据库中:
对第一个迁移来说,其作用和调用db.create_all()方法一样。但在后续的迁移中,upgrade命令能把改动应用到数据库中,且不影响其中保存的数据。