【Flask笔记】第五节:数据库
旧的笔记,以前存放的笔记软件停止运营了,重新迁移到博客上,删除和修改一些过时的内容。
0x00 使用Flask-SQLAlchemy管理数据库
安装flask-sqlalchemy:
1 | pip install flask-sqlalchemy |
扩展初始化:
1 | from flask import Flask |
1. 连接数据库服务
常用的数据库URI格式:
DBMS | URI |
---|---|
PostgreSQL | postgresql://username:password@host/databasename |
MySQL | mysql://username:password@host/databasename |
Oracle | oracle://username:password@host:port/sidname |
SQLite(unix) | sqlite:////absolute/path/to/foo.db |
SQLite(Windows) | sqlite:///absolute\path\to\foo.db 或 r’sqlite:///absolute\path\to\foo.db’ |
SQLite(内存型) | sqlite:/// 或 sqlite:///:memory: |
开发过程使用SQLite,兼容Windos、Linux、MacOS的URI写法:
1 | import os,sys |
从环境变量获取URI的写法:
1 | app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL','sqlite:///'+os.path.join(app.root_path,'data.db')) |
2. 定义数据库模型
1 | class Note(db.Model): |
SQLAlchemy常用字段类型:
字段 | 说明 |
---|---|
Integer | 整数 |
String | 字符串,可选参数length可以用来设置最大长度 |
Text | 较长的Unicode文本 |
Date | 日期,存储Python的datetime.date对象 |
Time | 时间,存储Python的datetime.time对象 |
DateTime | 日期和时间,存储Python的datetime对象 |
Interval | 时间间隔,存储Python的datetime.timedelta对象 |
Float | 浮点数 |
Boolean | 布尔值 |
PickleType | 存储Pickle列化的Python对象 |
LargeBinary | 存储任意二进制数据 |
一些既定的约束标准:
英文姓名长度70个字符,中文姓名20个字符,电子邮件地址255字符,用户名36个字符。一般都不超过255。
Flask-SQLAlchemy 生成表名字段名的规则:
1 | Message --> message #单个单词转化为小写 |
如果你想自己指定表名称,可以通过定义__tablename__
属性来实现。字段名默认为类属性名,可以通过字段类的构造方法的第一个参数指定,或使用关键字name
指定。
除了name
参数,实例化字段类时候常用的参数:
参数名 | 说明 |
---|---|
primary_key | 设置是否为主键 |
unique | 设置是否允许重复 |
index | 设置是否创建索引 |
nullabel | 设置是否可以为空 |
default | 设置默认值 |
3. 创建数据库和表
1 | flask shell |
数据库和表一旦创建之后,对模型的改动不会自动作用到实际的表中,再次调用create_all()
也不会更新表结构。如果要使改动生效,最简单的方式是调用db.drop_all()
删除数据库和表,然后再调用create_all()
方法创建。
实现创建数据库和表的flask命令
1 | import click |
在命令行中使用:
1 | flask initdb |
0x01 数据库操作
1. Create
- 创建Python对象
- 添加新创建的记录到数据库会话
- 提交数据库会话
1 | from app import db,Note |
除了依次调用
add()
方法添加多个记录,也可以使用add_all()
一次添加包含所有记录对象的列表
2. Read
一个完整的查询遵循下面的模式:
<模型类>.query.<过滤方法>.<查询方法>
常用的SQLAlchemy查询方法:
查询方法 | 说明 |
---|---|
all() | 返回包含所有查询记录的列表 |
first() | 返回查询的第一条记录,如果未找到,则返回None |
one() | 返回第一条记录,且仅允许有一条记录,如果记录数量大于1或小于1,则抛出错误 |
get(id) | 传入主键值作为参数,返回指定主键值的记录,如果未找到则返回None |
count() | 返回查询结果的数量 |
one_or_none() | 类似one(),如果结果数量不是1,则返回None |
first_or_404() | 返回查询的第一条记录,如果未找到,则返回404错误响应 |
get_or_404(id) | 传入主键值作为参数,返回指定主键值的记录,如果未找到则返回404错误响应 |
paginate() | 返回一个Pagination对象,可以对记录进行分页处理 |
with_parent(instance) | 传入模型类实例作为参数,返回和这个实例相关联的对象 |
查询例子
1 | all() #返回所有记录 Note.query. |
常用的SQLAlchemy过滤方法:
过滤方法 | 说明 |
---|---|
filter() | 使用指定的规则过滤记录,返回新产生的查询对象 |
filter_by() | 使用指定的规则过滤记录(以关键字表达式的形式),返回新产生的查询对象 |
order_by() | 使用指定条件对记录进行排序,返回新产生的查询对象 |
limit(limit) | 使用指定的值限制原查询返回记录的数量,返回新产生的查询对象 |
group_by() | 根据指定条件对记录进行分组,返回新产生的查询对象 |
offset(offset) | 使用指定的值偏移原查询结果,返回新产生的查询对象 |
过滤例子
1 | Note.query.filter(Note.body == 'SHAVE').first() |
在filter()方法中传入表达式时,除了==
以及表示不等于的!=
,其他常用查询操作符以及使用示例如下:
LIKE
1 | filter(Note.body.like('%foo%')) |
IN
1 | filter(Note.body.in_(['foo','bar','baz'])) |
NOT IN
1 | filter(~Note.body.in_(['foo','bar','baz'])) |
AND
1 | #使用and_() |
OR
1 | from sqlalchemy import or_ |
filter_by()
方法中,你可以使用关键词表达式来指定过滤规则,更方便的是你可以在这个过滤器中直接使用字段名称。
1 | 'SHAVE').first() Note.query.filter_by(body= |
3. Update
1 | 2) note = Note.query.get( |
只有插入新记录或者将现有记录添加到会话时才需要使用add()方法。
4. Delete
1 | 2) note = Note.query.get( |
0x02 在视图函数中操作数据库
0x03 定义关系
在关系型数据库中,我们可以通过关系让不同表之间的字段建立联系。一般来说,定义关系需要两步,分别是创建外键和定义关系属性。在更复杂的多对多关系中,我们还需要定义关联表来管理关系。
1. 一对多
以文章和作者来演示一对多关系:一个作者可以写多篇文章。
先定义模型:
1 | class Author(db.Model): |
1.1 定义外键
外键总是在“多”这一侧定义。
1 | class Article(db.Model): |
db.ForeignKey()
定义外键,传入关系另一侧的表名和主键字段名,即“author.id”。
1.2 定义关系属性
关系属性在关系的出发侧定义,即一对多关系的“一”这一侧。
1 | class Author(db.Model): |
db.relationship()
关系函数定义关系属性,函数的第一个参数为关系另一侧的模型名称。
下面我们会在Python Shell中演示如何对实际的对象建立关系。我们先创建一个作者记录和两个文章记录,并添加到数据库会话中:
1 | 'Foo') foo = Author(name = |
1.3 建立关系
建立关系有两种方式,第一种方式是为外键字段赋值,比如:
1 | 1 spam.author_id = |
我们将spam和ham对象的author_id字段的值设为1.这会和id字段为1的Author对象建立关系,在提交数据改动后,如果我们对id为1的foo对象调用articles关系属性,会看到spam和ham对象在返回的Article对象列表中:
1 | foo.articles |
另一种是通过操作关系属性,将关系属性赋给实际的对象即可建立关系。集合关系属性可以像列表一样操作,调用append()
方法来与一个Article对象建立关系:
1 | foo.articles.append(spam) |
对关系属性调用remove()
方法可以与对应对Article
对象解除关系:
1 | foo.articles.remove(spam) |
在关系函数中,有很多参数可以用来设置调用关系属性进行查询时的具体行为。常用的SQLAlchemy关系函数参数:
参数名 | 说明 |
---|---|
back_populates | 定义反向引用,用于建立双向关系,在关系的另一侧也必须显式定义关系属性 |
backref | 定义反向引用,自动在另一侧建立关系属性,是back_populates的简化版 |
lazy | 指定如何加载相关记录 |
uselist | 指定是否使用列表形式的加载记录,设为False则使用标量(scalar) |
cascade | 设置级联操作 |
order_by | 指定加载相关记录时的排序方式 |
secondary | 在多对多关系中指定关联表 |
primaryjoin | 指定多对多关系中的一级联结条件 |
secondaryjoin | 指定多对多关系中的二级联结条件 |
加载方式Lazy参数的常用选项:
关系加载方式 | 说明 |
---|---|
select | 在必要时一次性加载记录,返回包含记录的列表(默认值),等同于Lazy=True |
joined | 和父查询一样加载记录,但使用联结,等同于Lazy=False |
immediate | 一旦父查询加载就加载 |
subquery | 类似于joined,不过将使用子查询 |
dynamic | 不直接加载记录,而是返回一个包含相关记录的query对象,以便再继续附加查询函数对结果进行过滤 |
dynamic选项仅用于集合关系属性,不可用于多对一,一对一或是在关系函数中将uselist参数设置为False的情况。
1.4 建立双向关系
两侧都添加关系属性为双向关系,通过在关系另一侧也创建一个relationship()
函数,我们就可以在两个表之间建立双向关系。
1 | class Writer(db.Model): |
back_populates
参数的值需要设置为关系另一侧的关系属性名。
1 | 'Stephen King') king = Writer(name = |
设置双向关系后,除了通过集合属性books
来操作关系,也可以使用标量属性writer
来进行关系操作。
1 | carrie.writer = king |
相对的,将某个Book
的writer
属性设置为None
,就会解除与对应Writer
对象的关系,并且我们只需要在关系的一侧操作关系:
1 | None carrie.writer = |
1.5 使用backref简化关系定义
使用关系函数中的backref
参数可以简化双向关系的定义。
1 | class Singer(db.Model): |
在定义集合属性songs
的关系函数中,我们将backref
参数设为singer
,这会同时在Song
类中添加一个singer
标量属性。
需要注意的是,使用backerf
允许我们仅在关系一侧定义另一侧的关系属性,但是某些情况下,我们希望可以对在关系另一侧的关系属性进行设置,这时就需要使用db.backerf()
函数。
1 | class Singer(db.Model): |
2. 多对一
一对多关系反过来就是多对一关系,在下方示例程序中,Citizen
类表示居民,City
类表示城市:
1 | class Citizen(db.Model): #多 |
在多对一关系中外键和关系属性都定义在“多”这一侧,这时定义的city
关系属性是一个标量属性(返回单一数据)。
3. 一对一
一对一关系实际上通过建立双向关系的一对多关系基础上转化而来。我们要确保关系两侧的关系属性都只返回单个值,所以要在定义集合属性的关系函数中将uselist
参数设置为False
,这时一对多关系将被转换为一对一关系。
1 | class Country(db.Model): #一 |
多对多
在多对多关系中,每个记录都与关系另一侧的多个记录建立关系,除了关系两侧的模型外,我们还需要创建一个关联表。关联表不存储数据,只用来存储关系两侧模型的外键对应关系。
1 | # 关联表 |
关联表使用db.Table
来定义,第一个参数是关联表的名称。另外定义了两个外键,在student_id
中存储了Student
类的主键,teacher_id
中存储了Teacher
类的主键。
我们在Student类中定义了teahcers
关系属性来获取老师的集合。在多对多关系中定义了关系函数,除了第一个参数是关系另一侧的模型名称外,我们还需要添加一个secondary
参数,把这个值设为关联表名称。
和其他关系属性一样,当需要添加数据的时候,对关系属性使用append()
方法即可,如果想解除关系,那么使用remove()
方法。
0x04 更新数据库表
使用Flask-Migrate迁移数据库
安装依赖:
1 | $ pipenv install flask-migrate |
初始化操作:
1 | from flask import Flask |
创建迁移环境
1 | $ flask db init |
迁移环境只需要创建一次,这会在你的项目根目录下创建一个migrations文件夹,其中包含了自动生成的配置文件和迁移版本文件夹。
生成迁移脚本
1 | $ flask db migrate -m "add note timestamp" |
更新数据库
1 | $ flask db upgrade |
如果你想回滚迁移,那么可以使用downgrade
命令,它会撤销最后一次迁移在数据库中的改动。
0x05数据库进阶实战
级联操作
Cascade意为“级联操作”,就是操作一个对象的同时,对相关的对象也执行某些操作。我们通过一个Post模型和Comment模型来演示级联操作。
1 | class Post(db.Model): |
级联行为通过关系函数relationship()
的cascade
参数设置,我们希望在操作Post对象时,处于附属地位的Comment对象也相应执行某些操作,这时应该在Post类的关系函数中定义级联参数。设置了cascade
参数的一侧将被视为父对象,相关的对象则被视为子对象。
cascade通常使用多个组合值,级联值之间使用逗号分隔:
1 | class Post(db.Model): |
常用的配置组合如下所示:
- save-update,merge
- save-update,merge,delete
- all
- all,delete-orphan
当没有设置cascade
参数时,会使用默认值save-update,merge
。all
等同于除了delete-orphan
以外所有可用值的组合,即save-update,merge,refresh-expire,expunge,delete
。
save-update
当cascade参数设为save-update时,如果使用db.session.add()方法将Post对象添加到数据库会话时,那么与Post相关的Comment对象也将被添加到数据库会话。
首先创建一个Post对象和两个Comment对象:
1 | >>> post1 = Post() |
将post1添加到数据库会话后,只有post1在数据库会话中:
1 | >>> db.session.add(post1) |
如果我们让post1与这两个Comment对象建立关系,那么这两个Comment对象也会自动被添加到数据库会话中:
1 | >>> post1.comments.append(comment1) |
当调用db.session.commit()
提交数据库会话时,这三个对象都会被提交到数据库中。
delete
如果某个Post对象被删除,那么按照默认行为,该Post对象相关联的所有Comment对象都将与这个Post对象取消关联,外键字段的值会被清空。如果Post类的关系函数中cascade
参数设为delete
时,这些相关的Comment会在关联的Post对象删除时被一并删除。当需要设置delete
级联时,我们会将级联值设为all
或save-update,merge,delete
。
1 | class Post(db.Model): |
先创建一个文章对象和两个评论对象,并建立关系:
1 | >>> post2 = Post() |
现在共有两条Post记录和四条Comment记录:
1 | >>> Post.query.all() |
如果删除文章对象post2
,那么对应的两个评论对象也会一并删除:
1 | >>> post2 = Post.query.get(2) |