Flask-SQLAlchemy相关与Flask-Migrate相关

    数据库按照一定规则保存应用数据,应用再发起查询,取回所需的数据。Web应用最常使用基于关系模型的数据库,这种数据库也称为SQL数据库,因为它们使用结构化查询语言SQL。不过近年来文档数据库和键-值对数据库成了流行的替代选择,这两种数据库合称为NoSQL数据库
 

SQL数据库

    关系型数据库把数据存储在表中,表为应用中不同的实体建模。
    表中的列数是固定的,行数是可变的。列定义表所表示的实体的数据属性。表中的行定义部分或所有列对应的真实数据。
    表中有个特殊的列,称为主键,其值为表中各行的唯一标识符。表中还可以有称为外键的列,引用同一个表或不同表某一行的主键。行之间的这种联系称为关系,是关系型数据库模型的基础。
    

NoSQL数据库

    NosSQL数据库一般使用集合代替表,使用文档代替记录。NoSQL数据库采用的设计方式使联结变得困难,所以多数根本不支持联结。
    减少了表的数量,却增加了数据重复量。数据重复可以提升查询速度。
 

    SQL数据库擅长用高效且紧凑的形式存储结构化数据,关系型数据库采用ACID的范式,及原子性、一致性、隔离性、持续性。
    NoSQL数据库放宽了ACID的要求,从而获得性能上的优势。
    python数据库框架特点:易用性、性能、可移植性、Flask集成度

使用Flask-SQLAlchemy管理数据库

    Flask-SQLAlchemy简化了在Flask应用中使用SQLAlchemy的操作,既支持高层的ORM,也提供了使用数据库原生SQL的低层功能。
    在Flask-SQLAlchemy中数据库使用URL指定。 
MySQL
mysql://username:password@hostname/database
Postgres
postgresql://username:password@hostname/database
SQLite(linux等)
sqlite:///absolute/path/to/database
SQLite(win)
sqlite:///c:/absolute/path/to/database
 
    在这些URL中,hostname表示数据库服务所在的主机,可以是本地主机(localhost),也可以是远程服务器。数据服务器上可托管多个数据库,因此database表示使用数据库的名字。如果数据库需要验证身份,使用username和password提供数据库用户的凭证。
from flask_sqlalchemy import SQLAlchemy
from flask import Flask
app = Flask(__name__)
 
app.config["SQLALCHEMY_DATABASE_URI"] = 'mysql://username:password@hostname/database'
 
# 自动追踪,同步修改模型和数据库中的改动,建议设为False,降低内存消耗
app.config[SQLALCHEMY_TRACK_MODIFICATIONS] = False
 
# db是SQLAlchemy类的实例,表示应用使用的数据库
db = SQLAlchemy(app)

定义模型

    模型这个术语表示应用使用的持久化实体。在ORM中,模型一般是python中的类,类中的属性对应于数据库的列。
    两种常见的命名方式:数据库名字缩写_表名和tbl_表名
class Role(db.Model):
    __tablename__ = "tbl_role"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(32), unique=True)
    users = db.relationship("User", backref="role")  # 不是数据库中真实存在,建立两个表的关系,可通过role访问user,backref用于另一个表反推这个表
    
    def __repr__(self):
    """定义后,可以让显示对象时更直观,自定义显示信息"""
        return "Role object: name=%s" % self.name
 
class User(db.Model):
    """用户表"""
    __tablename__ = "tbl_users"  # 指明数据库表名
    id = db.Column(db.Integer, primary_key=True)  # 指明表的主键,整型的主键会默认设置为自增主键
    name = db.Column(db.String(64), unique=True)  # 指明真实在数据库创建的字段类型,限制长度,不可重复出现
    password = db.Column(db.String(128))
    role_id = db.Column(db.Integer, db.ForeignKey("tbl_role.id"))  # 指明外键,用于反推
    类变量__tablename__定义在数据库中使用的表名,如果未定义,Flask-SQLAlchemy会根据模型类产生一个默认名称,但默认表名没有遵守流行的使用复数命名的约定。其余的类变量都是该模型的属性,定义为db.Column类的实例或relationship关系对象。
    db.Column类构造函数的第一个参数是数据库列和模型属性的类型。
 

最常见的SQLAlchemy列类型:

字段类型名
python中的类型
说明
Integer
int
普通整数,一般32位
SmallInteger
int
取值范围小的整数,一般16位
BigInteger
int或long
不限制精度的整数
Float
float
浮点数
Numeric
decimal.Decimal
普通整数,一般32位
String
str
变长字符串
Text
str
变长字符串,对较长或不限长度的字符串做了优化
Unicode
unicode
变长Unicode字符串
Unicode Text
unicode
变长Unicode字符串,对较长或不限长度的字符串做了优化
Boolean
bool
布尔值,在mysql中是tinyint类型
Date
datetime.date
日期
Time
datetime.time
时间
DateTime
datetime.datetime
日期和时间
Interval
datetime.timedelta
时间间隔
Enum
str
一组字符串
PickleType
任何python对象
自动使用Pickle序列化
LargeBinary
str
二进制文件blob
DECIMAL(p,d)  
在mysql中常用于保留准确精确度,适用于货币等
    db.Column的其余参数指定属性的配置选项。
 

最常用的SQLAlchemy列选项:

列选项
说明
primary_key
如果为True,代表表的主键
unique
如果为True,代表这列不允许出现重复的值
index
如果为True,为这列创建索引,提高查询效率
nullable
如果为True,允许有空值,如果为Flase,不允许有空值
default
为这列定义默认值
 

外键四种约束

ForeignKey的参数ondelete
说明
RESTRICT
当父表数据被删除,从表会拒绝删除
NO ACTIION
同上
CASCADE
父表数据删除,从表数据也会跟着删除
SET NULL
父表数据删除,从表外键字段设为NULL
 

关系

    关系型数据库使用关系把不同表中的行联系起来。
class Role(db.Model):
    ...
    users = db.relationship("User", backref="role")
 
class User(db.Model):
    ...
     role_id = db.Column(db.Integer, db.ForeignKey("tbl_role.id"))
    关系使用usrs表中的外键连接两行。添加到User模型中的role_id列被定义为外键,传给db.Foreignkey的参数‘tbl_role.id’表名这列的值是tbl_role表中相应行的id。
    db.relationship()的第一个参数表明这个关系的另一端是哪个模型。如果关联的模型类在其后定义,可使用字符串(模型类名)形式指定。backref参数向User模型中添加一个role属性,从而定义反向关系,通过User实例的这个role属性可以获取对应的Role模型对象,不用再通过外键查找。
    多数情况下,db.relationship()都能自行找到关系中的外键,但有时却无法确定哪一列是外键,如两个列都定义为外键时,需要其他额外的参数。

常用的SQLAlchemy关系选项:

关系选项名
说明
backref
在关系的另一模型中添加反向引用
primaryjoin
明确指定两个模型之间使用的联结条件;只需在模棱两可的关系中指定
lazy
指定如何加载相关记录,可选值有select(首次访问时按需加载)、immediate(源对象加载后加载)、joined(加载记录,但使用联结)、subquery(立即加载,但使用子查询)、noload(永不加载)、dynamic(不加载记录,但提供加载记录的查询)
uselist
如果为Flase,不使用列表而使用标量值,常用于一对一关系
order_by
指定关系中记录的排序方式
secondary
指定多对多中记录的排序方式
secondaryjoin
在SQLAlchemy中无法自行决定时,指定多对多关系中的二级联结条件
    除了一对多之外,还有其他类型。一对一关系可以用一对多关系表示,但调用db.relationship()时要把uselist设为False。多对一关系也可使用一对多表示,对调两个表即可,或者把外键和db.relationship()都放在多的这一侧。最复杂是多对多,需要用到第三张表,这个表称为关联表(联结表)。
 

数据库操作

创建表

db.create_all()函数将寻找所有db.Model的子类,然后在数据库中创建对应的表。如果数据库中表已经存在,那么db.create_all()不会重新创建或更新相应的表。如果修改模型后要把改动应用到现有的数据库,更新现有数据库表的蛮力方式是先删除旧表再重新创建(不可取)。
db.drop_all()  # 清除数据库中所有数据
 
db.create_all()  # 创建所有之前定义的模型类的表

插入行

role1 = Role(name="admin")  # 创建Role对象,id是自增的不需要定义
us1 = User(name="wang", password="123", role_id=role1.id)
us2 = User(name="zhou", password="456", role=role1)
    模型的构造函数就受的参数是使用关键字参数指定的模型属性初始值。注User中role属性也可以使用,虽然它不是真正的数据库列,但却是一对多关系的高级表示。无需设定id属性,在多数数据库中主键由数据库自身管理。现在这些对象只存在于python中,还未写入数据库,所以id还未赋值。
    对数据库的改动通过数据库会话管理,也称为事务。在Flask-SQLAlchemy中,会话由db.session表示。准备把对象写入数据库之前,要先将其添加到会话。
# 每次只添加一个
db.session.add(role1)
db.session.add(us1)
db.session.add(us2)
# 每次添加多个
db.session.add_all([role1, us1, us2])
    添加会话后,再调用db.session.commit()方法提交会话。
db.session.commit()
    数据库会话能保证数据库的一致性。提交操作使用原子方式把会话中的对象全部写入数据库。如果在写入会话的过程中发生了错误,那么整个会话都会失效。如果你始终把相关改动放在会话中提交,就能避免因部分更新导致的数据库不一致。
    数据库会话也可以回滚,调用db.session.rollback()后,添加到数据库会话中的所有对象都将还原到它们在数据库在中的状态(提交新数据前)。
db.session.rollback()

修改数据行

    在数据库会话上调用add()方法重新上传某属性,起到更新模型的作用。
us2.name = "zhao"
db.session.add(us2)
db.session.commit()

删除行

    数据库会话还有delete()方法,起到从数据库删除的作用。会话提交后数据库才会执行
db.session.delete(us2)
db.session.commit()

查询行

    每个模型类都有query对象。最基本的模型查询是使用all()方法取回对应表中的所有记录。
Role.query.all()
User.query.all()
    使用过滤器可以配置query对象进行更精确的数据库查询。
    若想查看SQLAlchemy为查询生成的原生SQL查询语句,只需把query对象转换成字符串,str(User.query.all())强制类型转换。
 

query对象常用的SQLAlchemy查询过滤器:

过滤器
说明
filter()
把过滤器添加到原查询上,返回一个新查询
filter_by()
把等值过滤器添加到原查询上,返回一个新查询
limit
指定数值限定原查询返回的结果数量,返回一个新查询
offset()
偏移原查询返回的结果,返回一个新查询
order_by()
根据指定条件对原查询结果进行排序,返回一个新查询
group_by()
根据指定条件对原查询结果进行分组,返回一个新查询

query对象最常用的SQLAlchemy查询执行方法:

执行器
说明
all()
以列表形式返回查询的所有结果
first()
返回查询的第一个结果,如果未查到,返回None
first_or_404()
返回查询的第一个结果,如果未查到,返回404错误响应
get()
返回指定主键对应的行,如不存在,返回None
get_or_404()
返回指定主键对应的行,如不存在,返回404错误响应
count()
返回查询结果的数量
paginate()
返回一个Paginate对象,它包含指定范围内的结果
one()
返回查询的第一个结果,如果未查到,抛出异常
one_or_none()
返回查询的第一个结果,如果未查到,返回None
scalar()
返回对应一行一列的查询结果
# 通过模型类的query对象
Role.query.all()  # 查询多条对象,返回列表
ROle.query.first()  # 查询第一个对象
Role.query.get("主键id值")  # 查询对应主键值的对象
# 通过数据库会话的query对象
db.session.query(Role).all()  # 查询多个实例对象,查询执行方法与上述方式相同
from flask_sqlalchemy import _or
 
User.query.filter_by(name="wang").first().name  # 获取通过过滤器后的查询对象的第一个值的name属性
User.query.filter(User.name=="wang", User.role_id==1).first()  # 过滤器中逗号表并列关系
User.query.filter(or_(User.name=="wang", User.role_id==1)).first()  # or_函数表或关系
User.query.filter(User.role_id.in_([1,2,3]))  # in_函数表包含关系
User.query.filter(~User.role_id.in_([1,2,3]))  # ~表反
User.query.filter(User.role_id.is_(1))  # is_函数表判断关系
User.query.filter(User.role_id.isnot())  # isnot函数表不为空关系
 
User.query.offset(2).limit(2).all()  # offset跳过两条,从第三条开始,limit取两条
 
User.query.order_by("-id").all()  # 按id排序
User.query.order_by(User.id.desc()).all()  # 按id降序的方式排序
User.query.order_by(User.id.asc()).all()  # 按id升序的方式排序,默认升序
#以查询内容进行分组,返回新的查询对象
db.session.query(User.role_id, func.count(User.role_id)).group_by(User.role_id).all()
Role.query.get(1).users  # 关联查询
User.query.get(1).role  # 通过backref对应的字符反向关联查询
 
users = role1.users  # 隐式的查询会调用all()方法
users[0].role
    role1.users隐式调用all()方法,而query是隐藏的,无法指定更精确的查询过滤器。可在定义关系对象relationship()中添加lazy='dynamic'参数,从而禁止自动执行查询。此时role1.users返回一个查询query对象,可手动添加过滤器再调用all()方法。
 

Flask-Migrate实现数据库迁移

    在开发过程中,需要修改数据库模型,而且还要在修改之后更新数据库,最直接的方式就是删除旧表,但这样会丢失数据。更好的解决办法就是使用数据库迁移框架,它可以追踪数据库的变化,然后把变动以增量的方式应用到数据库中。
    在flask中可以使用Flask-Migrate扩展,来实现数据迁移,并且集成到Flask-Script中,所有操作通过命令就能完成。
      为了导出数据库迁移命令,Flask-Migrate提供了一个MigrateCommand类,可以附加到flask-script的manager对象

创建迁移仓库

from flask_migrate import Migrate
# ...
app = Flask(__name__)
manager = Manager(app)  # 创建脚本管理对象
db = SQLAlchemy(app)
 
migrate = Migrate(app, db)
 
# 在flask-script中添加一个db命令,与上面创建的db数据库对象无关
manager.add_command('db', MigrateCommand)
    为了开放数据库迁移相关的命令,Flask-Migrate添加了flask db命令和几个子命令。在新项目中可以使用init子命令添加数据库迁移支持
flask db init  # 创建migrations文件夹,所有迁移文件都放在里面
    这个命令会创建migrations目录,所有迁移脚本都存放在这里,如果是通过git checkout检出示例项目的,那无须做这一步,因为GitHub仓库中已有迁移仓库。数据库迁移仓库中的文件要和应用的其他文件一起纳入版本控制。

创建迁移脚本

    在Alembic中,数据库迁移用迁移脚本表示,脚本中有两个函数,分别是upgrade()和downgrade()。upgrade()函数把迁移中的改动应用到数据库中,downgrade()函数则将改动删除。Alembic具有添加和删除改动的能力,意味着数据库可重设到修改历史的任意一点。
    我们可以使用revision命令手动创建Alembic迁移,也可使用migrate命令自动创建。手动创建的迁移只是一个骨架,upgrade()和downgrade()函数都是空的,开发者要使用Alembic提供的Operations对象指令实现具体操作。自动创建的迁移会根据模型定义和数据库当前状态的差异尝试生成upgrade()和downgrade()函数内容。
    自动创建的迁移不一定总是正确,如我们重命名了一列,自动生成的迁移可能会把这当作删除的一列,然后又新增了一列。如果原封不动地使用自动生成的迁移,这一列中的数据就会丢失。
    使用Flask-Migrate管理数据库模式变化的步骤如下:
    1.对模型类做必要的修改
    2.执行flask db migrate 命令,自动创建一个迁移脚本
    3.检查自动生成的脚本,根据对模型的实际改动进行调整
    4.把迁移脚本纳入版本控制
    5.执行flask db upgrade 命令,把迁移应用到数据库中
flask db migrate -m "更改备注信息"  # 生成迁移文件
    如果一直使用git checkout 命令检出示例应用,那么无须执行migrate命令

更新数据库

    检查并修正好迁移脚本后,执行flask db upgrade命令,把迁移应用到数据库中。
flask db upgrade  # 上传更改,前进
    对第一个迁移来说,其作用与调用db.create_all()方法一样,但在后续的迁移中,flask db upgrade 命令能把改动应用到数据库,且不影响其中保存的数据。
    若已执行过db.create_all(),创建了数据库文件,此时flask db upgrade命令失败,可使用flask db stamp 命令把现有数据库标记为已更新。

添加几个迁移

    在开发项目的过程中,时常要修改数据库模型。如果使用迁移框架管理数据库,必须在迁移脚本中定义所有改动,否则改动将不可复现。修改数据库的步骤与创建第一个迁移类似。
    1.对数据库模型做必要修改
    2.执行flask db migrate 命令,生成迁移脚本
    3.检查自动生成的脚本,改正不准确的地方
    4.执行flask db upgrade 命令,把改动应用到数据库
    实现一个功能,可能要多次修改数据库模型才能得到预期结果。如果前一个脚本还未提交到源码控制系统中,可以继续在那个迁移中修改,以免创建大量无意义的小迁移脚本。
    在前一个迁移脚本的基础上修改步骤如下:
    1.执行flask db downgrade 命令,还原前一个脚本对数据库的改动(可能导致部分数据丢失)
    2.删除前一个迁移脚本
    3.执行flask db migrate 命令生成一个新的数据库迁移脚本。这个迁移脚本除了前面删除的那个脚本中的改动之外,还包括这一次对模型的改动
    4.根据前面的说明,检查并应用迁移脚本
flask db downgrade 版本库  # 删除更改,回退,若无版本号默认回退一步
 
flask xxx.py db history  # 查看历史记录