基于关系模型的数据库,称为SQL数据库。文档数据库和键值对数据库合称NoSQL数据库。
5.1 SQL数据库
关系数据库把数据存在表中。表的列数固定,行数可变。列定义实体的数据属性,行定义各列的真实数据。特殊的列,叫主键,其值为表中各行的唯一标识符。外键,引用同一个表或不同表中某行的主键。行之间的联系称为关系。
5.2 NoSQL数据库
NoSQL使用集合代替表,使用文档代替记录。每个用户存储一份完整的自己的数据库数据。
5.3 使用SQL还是NoSQL?
看情况
5.4 Python数据库框架
如果市面上常用数据库包无法满足需求,还有数据库抽象层代码包,例如SQLAlchemy和MongoEngine。可以使用抽象包直接处理高等级的Python对象,而不用处理表、文档或查询语言此类的数据库实体。
抽象层,也叫对象关系映射(Object-Relational Mapper, ORM)或对象文档映射(Object-Document Mapper,ODM),在用户不知觉的情况下把高层的面向对象操作转换为低层的数据库指令。
5.5 Flask-SQLAlchemy管理数据库
SQLAlchemy提供了高层ORM,也提供了使用数据库原生SQL的低层功能。
pip install flask-sqlalchemy
在Flask-SQLAlchemy中,数据库使用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
其中:
hostname表示MySQL服务所在的主机(本地localhost或远程服务器)
database表示数据库名
username和password表示数据库用户密令
程序使用的数据库URL保存到Flask配置对象的SQLALCHEMY_DATABASE_URI键中。选项SQLALCHEMY_COMMIT_TEARDOWN键设为True,每次请求结束后自动提交数据库变动。
from flask_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)
5.6 定义模型
在ORM中,模型一般是一个Python类,类中的属性对应数据库表中的列。
Flask-SQLAlchemy创建的数据库实例为模型提供了一个基类以及一系列辅助类和辅助函数,用于定义模型的结构。
hello.py:定义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__定义数据库中使用的表名;其余类变量为模型的属性,被定义为db.Column类的实例。常用的SQLAlchemy列类型自行查阅。SQLAlchemy列选项:primary_key主键、unique不允许出现重复的值、index为此列创建索引、nullable允许使用空值、default默认值。__repr()__方法,返回一个具有可读性的字符串表示模型,可在调试和测试使用。
注意:Flask-SQLAlchemy要求每个模型都要定义主键,这一列经常命名为id。
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'))
添加到User模型中的role_id列被定义为外键。传给db.ForeignKey()的参数roles.id表明列的值是roles表中行的id值。
添加到Role模型中的users属性代表关系的面向对象视角。users属性返回与角色相关联的用户组成的列表。db.relationship()的第一个参数表明关系的另一端是User模型,第二个参数的backref向User模型添加一个role属性,从而定义反向关系;这一属性可替代role_id访问Role模型。
SQLAlchemy关系选项
backref:在关系的另一个模型中添加反向引用
primaryjoin:指定两个模型之间使用的联结条件
lazy:指定如何加载相关记录,select首次访问按需加载,immediate源对象加载后加载,joined加载记录但使用联结,subquery立即加载但使用子查询,noload永不加载,dynamic不加载记录但提供加载记录的查询
uselist:使用列表还是标量值
order_by:排序方式
secondary:多对多关系中关系表的名字
secondaryjoin:指定多对多关系的二级联结条件
5.8 数据库操作
5.8.1 创建表
python
>>>from hello import db
>>>db.create_all()
此时创建了名为data.sqlite文件
更新现有数据库表的粗暴方式是先删表再创建
>>>db.drop_all()
>>>db.create_all()
5.8.2 插入行
在Python中添加角色和用户
>>> 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)
模型的构造函数,参数使用关键字参数指定的模型属性初始值。此时还没有写入数据库。
通过数据库会话管理数据库改动,在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
提交方式使用原子方式,如果写入会话的过程发生错误,整个会话失效。数据库会话也叫回滚,调用db.session.rollback()后,添加到数据库会话中的所有对象都会还原到在数据库时的状态。
5.8.3 修改行
>>> admin_role.name='Administrator'
>>> db.session.add(admin_role)
>>> db.session.commit()
此处将Admin角色重命名为Administrator
5.8.4 删除行
>>> db.session.delete(mod_role)
>>> db.session.commit()
此处将Moderator角色从数据库删除
5.8.5 查询行
5.8.5.1 Flask-SQLAlchemy为每个模型类都提供了query对象。
>>> Role.query.all()
[<Role u'Administrator'>, <Role u'User'>]
>>> User.query.all()
[<User u'john'>, <User u'susan'>, <User u'david'>]
5.8.5.2 过滤器可以配置query对象进行更精确的查询
>>> User.query.filter_by(role=user_role).all()
[<User u'susan'>, <User u'david'>]
5.8.5.3 可以将query对象去掉all(),转换为字符串显示原生SQL语句
>>> 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 \nFROM users \nWHERE ? = users.role_id'
5.8.5.4
如果退出shell,则Python对象的形式消失,需要发起查询来加载数据
>>> user_role = Role.query.filter_by(name='User').first()
5.8.5.5 常用查询过滤器
filter():过滤器添加到原查询,返回一个新查询
filter_by():等值过滤器添加到原查询,返回一个新查询
limit():限制原查询返回的结果数量,返回一个新查询
offset():偏移原查询返回的结果,返回一个新查询
order_by():对原查询结果排序,返回一个新查询
group_by():对原查询结果分组,返回一个新查询
5.8.5.6 触发查询执行
all():返回所有结果
first():返回第一个结果
first_or_404:返回第一个结果或者终止请求返回404
get():返回指定主键对应的行
get_or_404():返回指定主键对应的行,或者终止请求返回404
count():返回查询结果的数量
paginate:返回一个Paginate对象,包含指定范围内的结果
5.8.5.7 关系查询
>>> users = user_role.users
>>> users
[<User u'susan'>, <User u'david'>]
>>> users[0].role
<Role u'User'>
问题:user_role.users是隐含all()是自动查询,所以无法使用过滤器。
改进:
class Role(db.Model):
#...
users = db.relationship('User', backref='role', lazy='dynamic')
禁止自动执行之后就可以添加过滤器了。
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))
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
让Flask-Script的shell命令自动导入特定的对象。为shell命令注册一个make_context回调函数。
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
manager = Manager(app)
manager.add_command("shell", Shell(make_context=make_shell_context))
使用命令直接将数据库文件里的数据导入Python
python hello.py shell
测试:
>>> db
<SQLAlchemy engine=sqlite:yingshe/learning/data.sqlite>
5.11 使用Flask-Migrate实现数据库迁移
更新表的更好的方式是使用数据库迁移框架。Alembic集成到Flask-Script.
5.11.1 创建迁移仓库
pip install flask-migrate
hello.py
migrate = Migrate(app, db)
使用db命令
python hello.py db init,创建migrations文件夹
5.11.2 创建迁移脚本
数据库迁移用迁移脚本表示,upgrade()和downgrate()。
手工创建:revision命令加Operations对象指令
自动创建:migrate子命令
python hello.py db migrate -m "initial migration"
注意:自动迁移可能会漏掉内容,需要检查
5.11.3 更新数据库
检查并修正好迁移脚本后,使用db upgrade命令迁移
python hello.py db upgrade