文章目录
引言
数据库按照一定规则保存程序数据,程序再发起查询取回所需的数据。Web 程序最常用基于关系模型的数据库,这种数据库也称为SQL 数据库,因为它们使用结构化查询语言。不过最近几年文档数据库和键值对数据库成了流行的替代选择,这两种数据库合称NoSQL数据库。
1.SQL数据库
关系型数据库把数据存储在表中,表模拟程序中不同的实体。例如,订单管理程序的数据库中可能有表customers、products 和orders。表的列数是固定的,行数是可变的。列定义表所表示的实体的数据属性。例如,customers表中可能有name、address、phone 等列。表中的行定义各列对应的真实数据。表中有个特殊的列,称为主键,其值为表中各行的唯一标识符。表中还可以有称为外键的列,引用同一个表或不同表中某行的主键。行之间的这种联系称为关系,这是关系型数据库模型的基础。

关系型数据库的优点是存储数据很高效,而且避免了重复。。将这个数据库中的用户角色重命名也很简单,因为角色名只出现在一个地方。一旦在roles 表中修改完角色名,所有通过role_id 引用这个角色的用户都能立即看到更新。缺点是,把数据分别存放在多个表中还是很复杂的。生成一个包含角色的用户列表会遇到一个小问题,因为在此之前要分别从两个表中读取用户和用户角色,再将其联结起来。关系型数据库引擎为联结操作提供了必要的支持。
2.NoSQL数据库
所有不遵循关系模型的数据库统称为NoSQL 数据库。NoSQL 数据库一般使用集合代替表,使用文档代替记录。NoSQL 数据库采用的设计方式使联结变得困难,所以大多数数据库根本不支持这种操作。

NoSQL 数据库更适合设计成上图所示的结构。这是执行反规范化操作得到的结果,它减少了表的数量,却增加了数据重复量。这种结构的数据库要把角色名存储在每个用户中。如此一来,将角色重命名的操作就变得很耗时,可能需要更新大量文档。使用NoSQL 数据库当然也有好处。数据重复可以提升查询速度。列出用户及其角色的操作很简单,因为无需联结。
3.使用SQL还是NoSQL
SQL 数据库擅于用高效且紧凑的形式存储结构化数据。这种数据库需要花费大量精力保证数据的一致性。NoSQL 数据库放宽了对这种一致性的要求,从而获得性能上的优势。对中小型程序来说,SQL 和NoSQL数据库都是很好的选择,而且性能相当。
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 开发的扩展是你的首选。
基于以上因素,我们选择使用的数据库框架是Flask-SQLAlchemy(http://pythonhosted.org/Flask-SQLAlchemy/),这个Flask 扩展包装了SQLAlchemy(http://www.sqlalchemy.org/)框架。
5.使用Flask-SQLAlchemy管理数据库
Flask-SQLAlchemy 是一个Flask 扩展,简化了在Flask 程序中使用SQLAlchemy 的操作。SQLAlchemy 是一个很强大的关系型数据库框架,支持多种数据库后台。SQLAlchemy 提供了高层ORM,也提供了使用数据库原生SQL 的低层功能。和其他大多数扩展一样,Flask-SQLAlchemy 也使用pip 安装:
(venv) $ pip install flask-sqlalchemy
在Flask-SQLAlchemy 中,数据库使用URL 指定。最流行的数据库引擎采用的数据库URL格式如表所示。

在这些URL 中,hostname 表示MySQL 服务所在的主机,可以是本地主机(localhost),也可以是远程服务器。数据库服务器上可以托管多个数据库,因此database 表示要使用的数据库名。如果数据库需要进行认证,username 和password 表示数据库用户密令。SQLite 数据库不需要使用服务器,因此不用指定hostname、username 和password。URL 中的database 是硬盘上文件的文件名。
程序使用的数据库URL 必须保存到Flask 配置对象的SQLALCHEMY_DATABASE_URI 键中。配置对象中还有一个很有用的选项,即SQLALCHEMY_COMMIT_ON_TEARDOWN 键,将其设为True时,每次请求结束后都会自动提交数据库中的变动。其他配置选项的作用请参阅Flask-SQLAlchemy 的文档。下面的示例展示了如何初始化及配置一个简单的SQLite 数据库。
from flask.ext.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_COMMIT_ON_TEARDOWN'] = True
db = SQLAlchemy(app)
db 对象是SQLAlchemy 类的实例,表示程序使用的数据库,同时还获得了Flask-SQLAlchemy提供的所有功能。
6.定义模型
模型这个术语表示程序使用的持久化实体。在ORM 中,模型一般是一个Python 类,类中的属性对应数据库表中的列。Flask-SQLAlchemy 创建的数据库实例为模型提供了一个基类以及一系列辅助类和辅助函数,可用于定义模型的结构。SQL数据库示例中的roles 表和users 表可定义为模型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
Flask-SQLAlchemy 要求每个模型都要定义主键,这一列经常命名为id。虽然没有强制要求,但这两个模型都定义了__repr()__ 方法,返回一个具有可读性的字符串表示模型,可在调试和测试时使用。
类变量__tablename__ 定义在数据库中使用的表名。如果没有定义__tablename__,FlaskSQLAlchemy 会使用一个默认名字,但默认的表名没有遵守使用复数形式进行命名的约定,所以最好由我们自己来指定表名。其余的类变量都是该模型的属性,被定义为db.Column
类的实例。db.Column 类构造函数的第一个参数是数据库列和模型属性的类型。下表列出了一些可用的列类型以及在模型中使用的Python 类型。

db.Column 中其余的参数指定属性的配置选项。下表列出了一些可用选项。

7.关系
关系型数据库使用关系把不同表中的行联系起来。SQL数据库的示例关系图表示用户和角色之间的一种简单关系。这是角色到用户的一对多关系,因为一个角色可属于多个用户,而每个用户都只能有一个角色。该一对多关系在模型类中的表示方法如下代码所示:
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 属性,从而定义反向关系。这一属性可替代role_id 访问Role 模型,此时获取的是模型对象,而不是外键的值。
大多数情况下,db.relationship() 都能自行找到关系中的外键,但有时却无法决定把哪一列作为外键。例如,如果User 模型中有两个或以上的列定义为Role 模型的外键,SQLAlchemy 就不知道该使用哪列。如果无法决定外键,你就要为db.relationship() 提供额外参数,从而确定所用外键。下表列出了定义关系时常用的配置选项。
| 选项名 | 说明 |
|---|---|
backref | 在关系的另一个模型中添加反向引用 |
primaryjoin | 明确指定两个模型之间使用的联结条件。只在模棱两可的关系中需要指定 |
lazy | 指定如何加载相关记录。可选值有select(首次访问时按需加载)、immediate(源对象加载后就加载)、joined(加载记录,但使用联结)、subquery(立即加载,但使用子查询),noload(永不加载)和dynamic(不加载记录,但提供加载记录的查询) |
uselist | 如果设为Fales,不使用列表,而使用标量值 |
order_by | 指定关系中记录的排序方式 |
secondary | 指定多对多关系中关系表的名字 |
secondaryjoin | SQLAlchemy 无法自行决定时,指定多对多关系中的二级联结条件 |
除了一对多之外,还有几种其他的关系类型。
- 一对一关系可以用前面介绍的一对多关系表示,但调用db.relationship() 时要把uselist 设为False,把“多”变成“一”。
- 多对一关系也可使用一对多表示,对调两个表即可,或者把外键和db.relationship() 都放在“多”这一侧。
- 最复杂的关系类型是多对多,需要用到第三张表,这个表称为关系表。
8.数据库操作
现在模型已经按照示例的SQL数据库关系图完成配置,可以随时使用了。学习如何使用模型的最好方法是在Python shell 中实际操作。
(1)创建表
首先,我们要让Flask-SQLAlchemy 根据模型类创建数据库。方法是使用db.create_all()函数:
(venv) $ python hello.py shell
>>> from hello import db
>>> db.create_all()
如果你查看程序目录,会发现新建了一个名为data.sqlite 的文件。这个SQLite 数据库文件的名字就是在配置中指定的。如果数据库表已经存在于数据库中,那么db.create_all()不会重新创建或者更新这个表。如果修改模型后要把改动应用到现有的数据库中,这一特性会带来不便。更新现有数据库表的粗暴方式是先删除旧表再重新创建:
>>> db.drop_all()
>>> db.create_all()
遗憾的是,这个方法有个副作用,它把数据库中原有的数据都销毁了。
(2)插入行
>>> 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属性并没有明确设定,因为主键是由Flask-SQLAlchemy 管理的。现在这些对象只存在于Python 中,还未写入数据库。因此id 尚未赋值。
通过数据库会话管理对数据库所做的改动,在Flask-SQLAlchemy 中,会话由db.session表示。
- 数据库会话db.session 和Flask session 对象没有关系。数据库会话也称为
事务。 - 数据库会话能保证数据库的一致性。提交操作使用原子方式把会话中的对象全部写入数据库。如果在写入会话的过程中发生了错误,整个会话都会失效。如果你始终把相关改动放在会话中提交,就能避免因部分更新导致的数据库不一致性。
- 数据库会话也可回滚。调用db.session.rollback() 后,添加到数据库会话中的所有对象都会还原到它们在数据库时的状态。
准备把对象写入数据库之前,先要将其添加到会话中(2种方式):
>>> 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
(3)修改、删除行
- 在数据库会话上调用add() 方法也能更新模型。
- 数据库会话还有个delete() 方法。删除与插入和更新一样,提交数据库会话后才会执行。
>>> admin_role.name = 'Administrator'
>>> db.session.add(admin_role)
>>> db.session.commit()
>>> db.session.delete(mod_role)
>>> db.session.commit()
(4)查询行
-
query 对象: 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 对象进行更精确的数据库查询。filter_by() 等过滤器在query 对象上调用,返回一个更精确的query 对象。多个过滤器可以一起调用,直到获得所需结果。下面这个例子查找角色为"User" 的所有用户:
>>> User.query.filter_by(role=user_role).all() [<User u'susan'>, <User u'david'>]下表列出了可在query 对象上调用的常用过滤器。完整的列表参见SQLAlchemy 文档(http://docs.sqlalchemy.org)。

- 过滤后触发查询执行的方法: 在查询上应用指定的过滤器后,通过调用all() 执行查询,以列表的形式返回结果。除了all() 之外,还有其他方法能触发查询执行。下表列出了执行查询的其他方法。

- 查询关系: 关系和查询的处理方式类似。下面这个例子分别从关系的两端查询角色和用户之间的一对多关系:
这个例子中的user_role.users 查询有个小问题。执行user_role.users 表达式时,隐含的查询会调用all() 返回一个用户列表。query 对象是隐藏的,因此无法指定更精确的查询过滤器。就这个特定示例而言,返回一个按照字母顺序排序的用户列表可能更好。在下面的代码中,我们修改了关系的设置,加入了lazy = ‘dynamic’ 参数,从而禁止自动执行查询(配置关系之后,user_role.users 会返回一个尚未执行的查询,因此可以在其上添加过滤器)。>>> users = user_role.users >>> users [<User u'susan'>, <User u'david'>] >>> users[0].role <Role u'User'>class Role(db.Model): # ... users = db.relationship('User', backref='role', lazy='dynamic') # ... >>> user_role.users.order_by(User.username).all() [<User u'david'>, <User u'susan'>] >>> user_role.users.count() 2 - 查看生成的原生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会话: 如果退出了shell 会话,前面这些例子中创建的对象就不会以Python 对象的形式存在,而是作为各自数据库表中的行。如果你打开了一个新的shell 会话,就要从数据库中读取行,再重新创建Python 对象。下面这个例子发起了一个查询,加载名为"User" 的用户角色:
>>> user_role = Role.query.filter_by(name='User').first()
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)
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 中创建数据库表。
10.集成Python shell
每次启动shell 会话都要导入数据库实例和模型。为了避免一直重复导入,我们可以做些配置,让Flask-Script 的shell 命令自动导入特定的对象。若想把对象添加到导入列表中,我们要为shell 命令注册一个make_context 回调函数。
hello.py:为shell 命令添加一个上下文
from flask.ext.script import Shell
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'>
2719



被折叠的 条评论
为什么被折叠?



