【Flask笔记】第五节:数据库

img

旧的笔记,以前存放的笔记软件停止运营了,重新迁移到博客上,删除和修改一些过时的内容。

0x00 使用Flask-SQLAlchemy管理数据库

安装flask-sqlalchemy:

1
pip install flask-sqlalchemy

扩展初始化:

1
2
3
4
5
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
db = SQLAlchemy(app)

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
2
3
4
5
6
7
8
9
10
11
12
13
14
import os,sys
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

WIN = sys.platform.startswith('win')
if WIN:
prefix = 'sqlite:///'
else:
prefix = 'sqlite:////'

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = prefix + os.path.join(app.root_path,'data.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False #关闭对模型修改的监控
db = SQLAlchemy(app)

从环境变量获取URI的写法:

1
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL','sqlite:///'+os.path.join(app.root_path,'data.db'))

2. 定义数据库模型

1
2
3
class Note(db.Model):
id = db.Column(db.Integer,primary_key = True)
body = db.Column(db.Text)

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
2
Message --> message #单个单词转化为小写
FooBar --> foo_bar #多个单词转换为小写并使用下划线分隔

如果你想自己指定表名称,可以通过定义__tablename__属性来实现。字段名默认为类属性名,可以通过字段类的构造方法的第一个参数指定,或使用关键字name指定。

除了name参数,实例化字段类时候常用的参数:

参数名 说明
primary_key 设置是否为主键
unique 设置是否允许重复
index 设置是否创建索引
nullabel 设置是否可以为空
default 设置默认值

3. 创建数据库和表

1
2
3
$ flask shell
>>> from app import db
>>> db.create_all()

数据库和表一旦创建之后,对模型的改动不会自动作用到实际的表中,再次调用create_all()也不会更新表结构。如果要使改动生效,最简单的方式是调用db.drop_all()删除数据库和表,然后再调用create_all()方法创建。

实现创建数据库和表的flask命令

1
2
3
4
5
6
import click
...
@app.cli.command()
def initdb():
db.create_all()
click.echo('Initialized database.')

在命令行中使用:

1
flask initdb

0x01 数据库操作

1. Create

  1. 创建Python对象
  2. 添加新创建的记录到数据库会话
  3. 提交数据库会话
1
2
3
4
5
6
7
8
>>> from app import db,Note
>>> note1 = Note(body = 'remember Sammy Jankis')
>>> note2 = Note(body = 'SHAVE')
>>> note3 = Note(body = 'DON\'T BELIEVE HIS LIES,HE IS THE ONE,KILL HIM')
>>> db.session.add(note1)
>>> db.session.add(note2)
>>> db.session.add(note3)
>>> db.session.commit()

除了依次调用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
2
3
4
>>> Note.query.all() #返回所有记录
>>> note1 = Note.query.first() #返回第一条记录
>>> note2 = Note.query.get(2) #返回指定主键值的记录
>>> Note.query.count() #返回记录数量

常用的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
2
3
4
5
6
7
8
9
#使用and_()
from sqlalchemy import and_
filter(and_(Note.body == 'foo',Note.title == 'FooBar'))

#或在filter()中加入多个表达式,使用逗号分隔
filter(Note.body == 'foo',Note.title == 'FooBar')

#或叠加调用多个filter()/filter_by()方法
filter(Note.body == 'foo').filter(Note.title == 'FooBar')

OR

1
2
from sqlalchemy import or_
filter(or_(Note.body = 'foo',Note.body == 'bar'))

filter_by()方法中,你可以使用关键词表达式来指定过滤规则,更方便的是你可以在这个过滤器中直接使用字段名称。

1
>>> Note.query.filter_by(body='SHAVE').first()

3. Update

1
2
3
>>> note = Note.query.get(2)
>>> note.body = 'SHAVE LEEF THIGH'
>>> db.session.commit()

只有插入新记录或者将现有记录添加到会话时才需要使用add()方法。

4. Delete

1
2
3
>>> note = Note.query.get(2)
>>> db.session.delete(note)
>>> db.session.commit()

0x02 在视图函数中操作数据库

0x03 定义关系

在关系型数据库中,我们可以通过关系让不同表之间的字段建立联系。一般来说,定义关系需要两步,分别是创建外键和定义关系属性。在更复杂的多对多关系中,我们还需要定义关联表来管理关系。

1. 一对多

以文章和作者来演示一对多关系:一个作者可以写多篇文章。

先定义模型:

1
2
3
4
5
6
7
8
9
class Author(db.Model):
id = db.Column(db.Integer,primary_key = True)
name = db.Column(db.String(70),unique = True)
phone = db.Column(db.String(20))

class Article(db.Model):
id = db.Column(db.Integer,primary_key = True)
title = db.Column(db.String(50),index = True)
body = db.Column(db.Text)

1.1 定义外键

外键总是在“多”这一侧定义。

1
2
3
class Article(db.Model):
...
author_id = db.Column(db.Integer,db.ForeignKey('author.id'))

db.ForeignKey()定义外键,传入关系另一侧的表名和主键字段名,即“author.id”。

1.2 定义关系属性

关系属性在关系的出发侧定义,即一对多关系的“一”这一侧。

1
2
class Author(db.Model):
articles = db.relationship("Article")

db.relationship()关系函数定义关系属性,函数的第一个参数为关系另一侧的模型名称。

下面我们会在Python Shell中演示如何对实际的对象建立关系。我们先创建一个作者记录和两个文章记录,并添加到数据库会话中:

1
2
3
4
5
6
>>> foo = Author(name = 'Foo')
>>> spam = Article(title = 'Spam')
>>> ham = Article(title = 'Ham')
>>> db.session.add(foo)
>>> db.session.add(spam)
>>> db.session.add(ham)

1.3 建立关系

建立关系有两种方式,第一种方式是为外键字段赋值,比如:

1
2
3
>>> spam.author_id = 1
>>> ham.author_id = 1
>>> db.session.commit()

我们将spam和ham对象的author_id字段的值设为1.这会和id字段为1的Author对象建立关系,在提交数据改动后,如果我们对id为1的foo对象调用articles关系属性,会看到spam和ham对象在返回的Article对象列表中:

1
>>> foo.articles

另一种是通过操作关系属性,将关系属性赋给实际的对象即可建立关系。集合关系属性可以像列表一样操作,调用append()方法来与一个Article对象建立关系:

1
2
3
>>> foo.articles.append(spam)
>>> foo.articles.append(ham)
>>> db.session.commit()

对关系属性调用remove()方法可以与对应对Article对象解除关系:

1
2
>>> foo.articles.remove(spam)
>>> db.session.commit()

在关系函数中,有很多参数可以用来设置调用关系属性进行查询时的具体行为。常用的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
2
3
4
5
6
7
8
9
10
11
12
class Writer(db.Model):
id = db.Column(db.Integer,primary_key = True)
name = db.Column(db.String(70),unique = True)

books = db.relatinoship('Book',back_populates = 'writer')

class Book(db.Model):
id = db.Column(db.Integer,primary_key = True)
name = db.Column(db.String(50),index = True)

writer_id = db.Column(db.Integer,db.ForeignKey('writer.id'))
writer = db.relatinoship('Writer',back_populates = 'books')

back_populates参数的值需要设置为关系另一侧的关系属性名。

1
2
3
4
5
6
7
>>> king = Writer(name = 'Stephen King')
>>> carrie = Book(name = 'Carrie')
>>> it = Book(name = 'IT')
>>> db.session.add(king)
>>> db.session.add(carrie)
>>> db.session.add(it)
>>> db.session.commit()

设置双向关系后,除了通过集合属性books来操作关系,也可以使用标量属性writer来进行关系操作。

1
2
3
4
5
>>> carrie.writer = king
>>> carrie.writer
>>> king.books
>>> it.writer = king
>>> king.books

相对的,将某个Bookwriter属性设置为None,就会解除与对应Writer对象的关系,并且我们只需要在关系的一侧操作关系:

1
2
3
>>> carrie.writer = None
>>> king.books
>>> db.session.commit()

1.5 使用backref简化关系定义

使用关系函数中的backref参数可以简化双向关系的定义。

1
2
3
4
5
6
7
8
9
10
11
class Singer(db.Model):
id = db.Column(db.Integer,primary_key = True)
name = db.Column(db.String(70),unique = True)

songs = db.relationship('Song',backref='singer')

class Song(db.Model):
id = db.Column(db.Integer,primary_key = True)
name = db.Column(db.String(50),index = True)

singer_id = db.Column(db.Integer,db.ForeignKey('singer.id'))

在定义集合属性songs的关系函数中,我们将backref参数设为singer,这会同时在Song类中添加一个singer标量属性。
需要注意的是,使用backerf允许我们仅在关系一侧定义另一侧的关系属性,但是某些情况下,我们希望可以对在关系另一侧的关系属性进行设置,这时就需要使用db.backerf()函数。

1
2
3
class Singer(db.Model):
...
songs = db.relationship('Song',backref = db.backref('singer',uselist = False))

2. 多对一

一对多关系反过来就是多对一关系,在下方示例程序中,Citizen类表示居民,City类表示城市:

1
2
3
4
5
6
7
8
9
class Citizen(db.Model): #多
id = db.Column(db.Integer,primary_key = True)
name = db.Column(db.String(70),unique = True)
city_id = db.Column(Integer,db.ForeignKey('city.id'))
city = db.relationship('City')

class City(db.Model): #一
id = db.Column(db.Integer,primary_key = True)
name = db.Column(db.String(30),unique = True)

在多对一关系中外键和关系属性都定义在“多”这一侧,这时定义的city关系属性是一个标量属性(返回单一数据)。

3. 一对一

一对一关系实际上通过建立双向关系的一对多关系基础上转化而来。我们要确保关系两侧的关系属性都只返回单个值,所以要在定义集合属性的关系函数中将uselist参数设置为False,这时一对多关系将被转换为一对一关系。

1
2
3
4
5
6
7
8
9
10
class Country(db.Model): #一
id = db.Column(db.Integer,primary_key = True)
name = db.Column(db.String(30),unique = True)
capital = db.relationship('Capital',uselist = False)

class Capital(db.Model): #多
id = db.Column(db.Integer,primary_key = True)
name = db.Column(db.String(30),unique = True)
country_id = db.Column(db.Integer,db.ForeignKey('country.id'))
country = db.relationship('Country')

多对多

在多对多关系中,每个记录都与关系另一侧的多个记录建立关系,除了关系两侧的模型外,我们还需要创建一个关联表。关联表不存储数据,只用来存储关系两侧模型的外键对应关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 关联表
association_table = db.Table(
'association',
db.Column('student_id',db.Integer,db.ForeignKey('student.id')),
db.Column('teacher_id',db.Integer,db.ForeignKey('teacher.id'))
)

class Student(db.Model):
id = db.Column(db.Integer,primary_key = True)
name = db.Column(db.String(70),unique = True)
grade = db.Column(db.String(20))

teachers = db.relationship('Teacher',secondary = association_table,back_populates = 'students')

class Teacher(db.Model):
id = db.Column(db.Integer,primary_key = True)
name = db.Column(db.String(70),unique = True)
office = db.Column(db.String(20))

students = db.relationship('Student',secondary = association_table,back_populates = 'teachers')

关联表使用db.Table来定义,第一个参数是关联表的名称。另外定义了两个外键,在student_id中存储了Student类的主键,teacher_id中存储了Teacher类的主键。
我们在Student类中定义了teahcers关系属性来获取老师的集合。在多对多关系中定义了关系函数,除了第一个参数是关系另一侧的模型名称外,我们还需要添加一个secondary参数,把这个值设为关联表名称。

和其他关系属性一样,当需要添加数据的时候,对关系属性使用append()方法即可,如果想解除关系,那么使用remove()方法。

0x04 更新数据库表

使用Flask-Migrate迁移数据库

安装依赖:

1
$ pipenv install flask-migrate

初始化操作:

1
2
3
4
5
6
7
8
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

app = Flask(__name__)
db = SQLAlchemy(app)

migrate = Migrate(app,db)

创建迁移环境

1
$ flask db init

迁移环境只需要创建一次,这会在你的项目根目录下创建一个migrations文件夹,其中包含了自动生成的配置文件和迁移版本文件夹。

生成迁移脚本

1
$ flask db migrate -m "add note timestamp"

更新数据库

1
$ flask db upgrade

如果你想回滚迁移,那么可以使用downgrade命令,它会撤销最后一次迁移在数据库中的改动。

0x05数据库进阶实战

级联操作

Cascade意为“级联操作”,就是操作一个对象的同时,对相关的对象也执行某些操作。我们通过一个Post模型和Comment模型来演示级联操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Post(db.Model):
id = db.Column(db.Integer,primary_key = True)
title = db.Column(db.String(50),unique = True)
body = db.Column(db.Text)

comment = db.relationship('Comment',back_populates = 'post')

class Comment(db.Model):
id = db.Column(db.Integer,primary_key = True)
body = db.Column(db.Text)

post_id = db.Column(db.Integer,db.ForeignKey('post.id')
post = db.relationship('Post',back_populates = 'comments')

级联行为通过关系函数relationship()cascade参数设置,我们希望在操作Post对象时,处于附属地位的Comment对象也相应执行某些操作,这时应该在Post类的关系函数中定义级联参数。设置了cascade参数的一侧将被视为父对象,相关的对象则被视为子对象。
cascade通常使用多个组合值,级联值之间使用逗号分隔:

1
2
class Post(db.Model):
comment = relationship('Comment',cascade = 'save-update,merge,delete')

常用的配置组合如下所示:

  • save-update,merge
  • save-update,merge,delete
  • all
  • all,delete-orphan
    当没有设置cascade参数时,会使用默认值save-update,mergeall等同于除了delete-orphan以外所有可用值的组合,即save-update,merge,refresh-expire,expunge,delete

save-update

当cascade参数设为save-update时,如果使用db.session.add()方法将Post对象添加到数据库会话时,那么与Post相关的Comment对象也将被添加到数据库会话。

首先创建一个Post对象和两个Comment对象:

1
2
3
>>> post1 = Post()
>>> comment1 = Comment()
>>> comment2 = Comment()

将post1添加到数据库会话后,只有post1在数据库会话中:

1
2
3
4
5
6
7
>>> db.session.add(post1)
>>> post1 in db.session
True
>>> comment1 in db.session
False
>>> comment2 in db.session
False

如果我们让post1与这两个Comment对象建立关系,那么这两个Comment对象也会自动被添加到数据库会话中:

1
2
3
4
5
6
>>> post1.comments.append(comment1)
>>> post1.comments.append(comment2)
>>> comment1 in db.session
True
>>> comment2 in db.session
True

当调用db.session.commit()提交数据库会话时,这三个对象都会被提交到数据库中。

delete

如果某个Post对象被删除,那么按照默认行为,该Post对象相关联的所有Comment对象都将与这个Post对象取消关联,外键字段的值会被清空。如果Post类的关系函数中cascade参数设为delete时,这些相关的Comment会在关联的Post对象删除时被一并删除。当需要设置delete级联时,我们会将级联值设为allsave-update,merge,delete

1
2
class Post(db.Model):
comments = relationship('Comment',cascade = 'all')

先创建一个文章对象和两个评论对象,并建立关系:

1
2
3
4
5
6
7
>>> post2 = Post()
>>> comment3 = Comment()
>>> comment4 = Comment()
>>> post2.comment.append(comment3)
>>> post2.comment.append(comment4)
>>> db.session.add(post2)
>>> db.session.commit()

现在共有两条Post记录和四条Comment记录:

1
2
3
4
>>> Post.query.all()
[<Post 1>,<Post 2>]
>>> Comment.query.all()
[<Comment 1>,<Comment 2>,<Comment 3>,<Comment 4>]

如果删除文章对象post2,那么对应的两个评论对象也会一并删除:

1
2
3
4
5
6
7
>>> post2 = Post.query.get(2)
>>> db.session.delete(post2)
>>> db.session.commit()
>>> Post.query.all()
[<Post 1>]
>>> Comment.query.all()
[<Comment 1>,<Comment 2>]

delete-orphan

事件监听