数据库
使用Flask-SQLAlchemy管理数据库
Flask-SQLAlchemy
是一个Flask扩展,简化了在Flask程序中使用SQLAlchemy的操作。SQLAlchemy是一个很强大的关系型数据库框架,支持多种数据库后台。SQLAlchemy提供了高层ORM,也提供了使用数据库原生SQL的低层功能。
使用pip安装此扩展pip install flask-sqlalchemy
在Flask-SQLAlchemy中,数据库使用URL指定。最流行的数据库引擎采用的数据库URL格式如表
数据库引擎 | URL |
---|---|
MySQL | mysql://username:password@hostname/database |
Postgres | postgresql://username:password@hostname/database |
SQLite(Unix) | sqlite:absolute/path/to/database |
SQLite(Windows) | sqlite:///c:/absolute/path/to/database |
在这些URL中,hostname 表示MySQL服务所在的主机,可以是本地主机(localhost),也可以是远程服务器。数据库服务器上可以托管多个数据库,因此database 表示要使用的数据库名。如果数据库需要进行认证,username和password表示数据库用户密令。
SQLite数据库不需要使用服务器,因此不用指定hostname、username和password。URL中的database 是硬盘上文件的文件名。
程序使用的数据库URL 必须保存到Flask 配置对象的SQLALCHEMY_DATABASE_URI
键中。配置对象中还有一个很有用的选项,即SQLALCHEMY_COMMIT_ON_TEARDOWN
键,将其设为True时,每次请求结束后都会自动提交数据库中的变动。
配置数据库如下:
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提供的所有功能。
这里我使用的是MySQL数据库与sqlite不同,MySQL使用,应将上面的SQLALCHEMY_DATABASE_URI
改为:mysql+pymysql://username:password@host/database
因此还要安装pymysql;pip install pymysql
定义模型
模型这个术语表示程序使用的持久化实体。在ORM中,模型一般是一个Python类,类中的属性对应数据库表中的列。Flask-SQLAlchemy创建的数据库实例为模型提供了一个基类以及一系列辅助类和辅助函数,可用于定义模型的结构。
如下所示,定义一个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
类构造函数的第一个参数是数据库列和模型属性的类型。下表列出了一些可用的列类型以及在模型中使用的Python类型。
类型名 | 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 | 二进制文件 |
db.Column
中其余的参数指定属性的配置选项如下:
选项名 | 说明 |
---|---|
primary_key | 如果设为True,这列就是表的主键 |
unique | 如果设为True,这列不允许出现重复的值 |
index | 如果设为True,为这列创建索引,提升查询效率 |
nullable | 如果设为True,这列允许使用空值;如果设为False,这列不允许使用空值 |
default | 为这列定义默认值 |
虽然没有强制要求,但这两个模型都定义了__repr()__
方法,返回一个具有可读性的字符串表示模型,可在调试和测试时使用。
关系
关系型数据库使用关系把不同表中的行联系起来。再上图所示的关系图表示用户和角色之间的一种简单关系。这是角色到用户的一对多关系,因为一个角色可属于多个用户,而每个用户都只能有一个角色。
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()都能自行找到关系中的外键,但有时却无法决定把哪一列作为外键。如果无法决定外键,你就要为db.relationship()提供额外参数,从而确定所用外键。如下所示:
选项名 | 说明 |
---|---|
backref | 在关系的另一个模型中添加反向引用 |
primaryjoin | 明确指定两个模型之间使用的联结条件。只在模棱两可的关系中需要指定 |
lazy | 指定如何加载相关记录。可选值有select(首次访问时按需加载)、immediate(源对象加载后就加载)、joined(加载记录,但使用联结)、subquery(立即加载,但使用子查询),noload(永不加载)和dynamic(不加载记录,但提供加载记录的查询) |
uselist | 如果设为Fales,不使用列表,而使用标量值 |
order_by | 指定关系中记录的排序方式 |
secondary | 指定多对多关系中关系表的名字 |
secondaryjoin | SQLAlchemy无法自行决定时,指定多对多关系中的二级联结条件 |
数据库操作
- 创建表
首先,我们要让Flask-SQLAlchemy根据模型类创建数据库。方法是使用db.create_all()
函数:
(venv) $ python hello.py shell
>>> from hello import db
>>> db.create_all()
在使用python shell时,需要将app
参数导入至flask-script中的Manager
类中:
from flask-script import Manager
# .....
manager = Manager(app)
#....
if __name__="__main__":
manager.run()
- 删除表
db.drop_all()
- 插入行
下面这些代码将创建Role和User
>>> 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属性也可使用,虽然它不是真正的数据库列,但却是一对多关系的高级表示。但是这些改动并未写入数据库中,需要通过数据库会话管理对数据库所做的改动,会话由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)
为了把对象写入数据库,我们要调用commit()
方法提交会话:
>>> db.session.commit()
数据库会话也可回滚。调用db.session.rollback()后,添加到数据库会话中的所有对象都会还原到它们在数据库时的状态。
- 修改行
在数据库会话上调用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对象。最基本的模型查询是取回对应表中的所有记录:
>>> Role.query.all()
[<Role u'Administrator'>, <Role u'User'>]
>>> User.query.all()
[<User u'john'>, <User u'susan'>, <User u'david'>]
若想看到原生的SQL查询语句,需要将其转换为字符串类型即可。
下表列出了可在query对象上调用的常用过滤器:
滤器 | 说明 |
---|---|
filter() | 把过滤器添加到原查询上,返回一个新查询 |
filter_by() | 把等值过滤器添加到原查询上,返回一个新查询 |
limit() | 使用指定的值限制原查询返回的结果数量,返回一个新查询 |
offset() | 偏移原查询返回的结果,返回一个新查询 |
order_by() | 根据指定条件对原查询结果进行排序,返回一个新查询 |
group_by() | 根据指定条件对原查询结果进行分组,返回一个新查询 |
在查询上应用指定的过滤器后,通过调用all()执行查询,以列表的形式返回结果。除了all()之外,还有其他方法能触发查询执行。下表列出了执行查询的其他方法:
方法 | 说明 |
---|---|
all() | 以列表形式返回查询的所有结果 |
first() | 返回查询的第一个结果,如果没有结果,则返回None |
first_or_404() | 返回查询的第一个结果,如果没有结果,则终止请求,返回404错误响应 |
get() | 返回指定主键对应的行,如果没有对应的行,则返回None |
get_or_404() | 返回指定主键对应的行,如果没找到指定的主键,则终止请求,返回404错误响应 |
count() | 返回查询结果的数量 |
paginate() | 返回一个Paginate对象,它包含指定范围内的结果 |
使用Flask-Migrate实现数据库迁移
在开发程序的过程中,你会发现有时需要修改数据库模型,而且修改之后还需要更新数据库。仅当数据库表不存在时,Flask-SQLAlchemy才会根据模型进行创建。因此,更新表的唯一方式就是先删除旧表,不过这样做会丢失数据库中的所有数据。
更新表的更好方法是使用数据库迁移框架。源码版本控制工具可以跟踪源码文件的变化,类似地,数据库迁移框架能跟踪数据库模式的变化,然后增量式的把变化应用到数据库中。
安装此扩展:pip install flask-migrate
使用方法如下:
from flask.ext.migrate import Migrate, MigrateCommand
# ...
migrate = Migrate(app, db)
manager.add_command('db', MigrateCommand)
在维护数据库迁移之前,要使用init
子命令创建迁移仓库:
(venv) $ python hello.py 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
文件夹,所有迁移脚本都存放其中。
数据库迁移用迁移脚本表示。脚本中有两个函数,分别是upgrade()
和downgrade()
。upgrade()函数把迁移中的改动应用到数据库中,downgrade()函数则将改动删除。
使用migrate命令自动创建,手动创建的迁移只是一个骨架,upgrade()和downgrade()函数都是空的。
migrate子命令用来自动创建迁移脚本:
(venv) $ python hello.py 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
我们可以使用db upgrade命令把迁移应用到数据库中:
(venv) $ python hello.py 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
本章完整程序
import os
from flask import Flask, render_template, session, redirect, url_for
from flask_bootstrap import Bootstrap
from flask_moment import Moment
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from flask_sqlalchemy import SQLAlchemy
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
app.config['SQLALCHEMY_DATABASE_URI'] =\
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
bootstrap = Bootstrap(app)
moment = Moment(app)
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
class NameForm(FlaskForm):
name = StringField('What is your name?', validators=[DataRequired()])
submit = SubmitField('Submit')
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
@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))