在Shell中操作
创建数据库
方法是用db.create_all()
函数:
(venv) $ python hello.py shell
>>> from hello import db
>>> db.creat_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)
现在还不能取到值,因为会话没有提交:
>>>print(admin_role.id)
None
>>>print(mod_role.id)
None
>>>print(user_role.id)
None
会话由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属性,它们已经赋值了
数据库会话
db.session
和之前介绍的Flasksession
对象没有关系,数据库会话也称为事务
数据库会话也可以回滚,调用db.session.rollback()
后,添加到数据库会话中的所有对象都会还原到它们在数据库时的状态
修改行
在数据库会话上调用add()
方法也能更新模型,把Admin角色重命名为Administrator:
>>> admin_role.name= 'Administrator'
>>> db.session.add(admin_role)
>>> db.session.commit()
删除与插入和更新一样,提交会话后才会执行
查询行
Flask-SQLAlchemy为每个模型类都提供了query
对象,最基本的模型查询是取回对应表中的所有记录:
>>> Role.query.all()
[<Role u'Administrator'>, <Role u'Moderator'>, <Role u'User'>, <Role u'john'>, <Role u'susan'>, <Role u'david'>]
>>> User.query.all()
[<User u'john'>, <User u'susan'>, <User u'david'>]
使用过滤器(filter_by()
)可以配置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))
退出Shell后的查询:下例发起了一个查询,加载名为User的用户角色:
>>> user_role = Role.query.filter_by(name='User').first()
filter_by()
等过滤器在query对象上调用,返回一个更精确的query对象,多个过滤器可以一起调用,知道获得所需结果
下表是可在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对象,它包含指定范围内的结果 |
关系和查询的处理方式类似,下例分别从关系的两端查询用户和角色之间的一对多关系:
>>> users = user_role.users
>>> users
[<User u'susan>,<User u'david'>]
>>> users[0].role
<Role 'User'>
这个例子中的user_role.users
查询有个小问题,执行user_role.users
表达式时,隐含的查询会调用all()
返回一个用户列表,query对象是隐藏的,因此无法指定更精确的查询过滤器,就这个特定示例而言,返回一个按照字母顺序排序的用户列表可能更好
下例中,我们修改了关系的设置,加入了lazy=’dynamic’参数,从而禁止自动执行查询
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
在视图函数中操作数据库
数据库操作可以直接在视图函数中进行,示例如下:
@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')),
form=form, name=session.get('name'),
known=session.get('known', False))
在这个修改后的版本中,提交表单后,程序会使用filter_by()
查询过滤器在数据库中查找提交的名字,变量known被写入用户会话中,因此重定向之后,可以把数据传给模板,用来显示自定义的欢迎消息,要想让程序正常运行,必须按照之前的方法,在Python shell中创建数据库表
下面是对应的模板新版本,使用known
参数在欢迎消息中加入了第二行,从而对已知用户和新用户显示不同的内容
# index.html
{% extends 'base.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% block page_content %}
<div class="page-header">
<h1>Hello, {% if name %} {{ name }}{% else %}Stringer{% 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-Script的shell命令自动导入特定的对象
若想把对象添加到导入列表中,我们要为shell命令注册一个make_context
回调函数,如下:
# 为shell命令添加一个上下文
from flask_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
>>> app
<Flask 'hello'>
>>> db
<SQLAlchemy engine='sqlite:///D:\\flasky\\env\\data.sqlite'>
>>> User
<class '__main__.User'>
>>>
使用Flask-Migrate实现数据库迁移
在开发程序的过程中,会发现有时需要修改数据库模型,而且修改之后还需要更新数据库
仅当数据库表不存在时,Flask-SQLAlchemy才会根据模型进行创建,因此更新表的唯一方式就是先删除旧表,不过这么做会丢失数据库中的所有数据
更好的方法是使用数据库迁移框架, 源码版本控制工具可以跟踪源码文件的变化,类似的,数据库迁移框架能跟踪数据库模式的变化,然后增量式的把变化应用到数据库中
SQLAlchemy的主力开发人员编写了一个迁移框架Alembic,除了直接使用Alembic之外,Flask程序还可以使用Flask-Migrate拓展,这个拓展对Alembic做了轻量级包装,并集成到Flask-Script中
创建迁移仓库
首先,我们要在虚拟环境中安装Flask-Migrate
(venv) % pip install flask-Migrate
然后配置
from flask_migrate import Migrate, MigrateCommand
# ...
migrate = Migrate(app, db)
manager.add_command('db', MigrateCommand)
# ...
为了导出数据库迁移命令,Flask-Migrate提供了一个MigrateCommand
类,可附加到Flask-Script的manager
对象上,在这个例子中,MigrateCommand
类使用db
命令附加
在维护数据库迁移之前,要使用init子命令创建迁移仓库
(env) PS D:\flasky\env> python hello.py db init
Creating directory D:\flasky\env\migrations ... done
Creating directory D:\flasky\env\migrations\versions ... done
Generating D:\flasky\env\migrations\alembic.ini ... done
Generating D:\flasky\env\migrations\env.py ... done
Generating D:\flasky\env\migrations\env.pyc ... done
Generating D:\flasky\env\migrations\README ... done
Generating D:\flasky\env\migrations\script.py.mako ... done
Please edit configuration/connection/logging settings in 'D:\\flasky\\env\\migrations\\alembic.ini' before proceeding.
这个命令会创建migrations文件夹,所有迁移脚本都存放在其中
数据库迁移仓库中的文件要和程序的其他文件一起纳入版本控制
创建迁移脚本
在Alembic中,数据库迁移用迁移脚本表示,脚本中有两个函数,分别是upgrade()
和downgrade()
,upgrade()
函数把迁移中的改动应用到数据库中,downgrade()
函数则将改动删除,Alembic具有增加和删除改动的能力,因此数据库可重设到修改历史的任意一点
我们可以使用revision
命令手动创建Alembic迁移,也可使用migrate
命令自动创建,手动创建的迁移只是个骨架,upgrade()
和downgrade()
都是空的,开发者要使用Alembic提供的Operations
对象指令实现具体操作,自动创建的迁移会根据模型定义和数据库当前状态之间的差异生成upgrade()
和downgrade()
函数的内容
自动创建的迁移不一定总是正确的,有可能会漏掉一些细节,自动生成迁移脚本后一定要进行检查
migrate
子命令用来自动创建迁移脚本
(env) PS D:\flasky\env> python hello.py db migrate -m 'initial migration'
更新数据库
检查并修正好迁移脚本后,我们可以使用db upgrade
命令把迁移应用到数据库中
(env) PS D:\flasky\env> python hello.py db upgrade
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
对第一个迁移来说,其作用和调用db.create_all()
方法一样,但在后续的迁移中,upgrade
命令能把改动应用到数据库中,且不影响其中保存的数据