概述
- 数据库是指由存储数据的多个或多个文件组成的集合,是一种容器;管理数据库的软件被称为DBMS;
- 数据库的分类:
- SQL 关系型数据库使用关系来定义数据对象,不同表之间使用关系连接。
- NoSQL 泛指不使用关系型数据库中的表格形式的数据库。NoSQL数据库被大量应用于实时WEB程序和大型程序中。与传统SQL相比,在速度和可扩展性方面有很大优势。最常用的2种NoSQL数据库:
- 文档存储:文档存储使用的文档类似SQL数据库中的记录,文档使用类JSON格式来表示数据,如MongoDB;
- 键值对存储:通过键来存取数据,在读取上非常快,通常用来存储临时内容,作为缓存使用。常见的键值对DBMS如Redis,不仅可以管理键值对数据库,还可以作为缓存后端和消息代理。
- 如何选择数据库?
- NoSQL数据库不需要定义表和列等结构,也不限定存储的数据格式,在存储方式上比较灵活,在特定场景下效率更高。
- SQL数据库稍显不同,但不容易出错,能够适用大部分的应用场景。
- 大型项目通常会同时需要多种数据库,比如使用MySQL作为主数据库存储用户资料和文章,使用Redis缓存数据,使用MongoDB存储实时消息。
ORM魔法
- 使用原生SQL语句操作数据库的2类问题:
- 降低代码的可读性,且容易引发安全问题,如SQL注入;
- 常见的开发模式是在开发时使用简单的SQLite,而在部署时切换到MySQL等更健壮的DBMS。但不同的DBMS需要使用不同的Python接口库,是的DBMS的切换变得不易;
- ORM把底层的SQL实体转换成高层的Python对象,主要实现了3层映射关系:
- 表 -> Python类
- 字段 -> 类属性
- 记录 -> 类实例
- 使用ORM具有以下优点:
- 灵活性好:既能使用高层对象来操作数据库,又支持原生SQL语句;
- 提升开发效率
- 可移植性好:ORM支持MySQl、Oracle等DBMS,只需改动少量配置,就可以随意更换DBMS;
使用Flask-SQLAlchemy管理数据库
- pipenv install flask-sqlalchemy
- from flask_sqlalchemy import SQLALchemy
- db = SQLAlchemy(app)
以上完成扩展的初始化,db代表我们的数据库,它可以使用SQLAlchemy提供的所有功能。大多数情况下,我们不需要手动从SQLAlchemy导入类或函数。在sqlalchemy和sqlalchemy.orm模块中实现的类和函数,以及其它几个常用的模块和对象都可以作为db对象的属性调用。
连接数据库服务器
- 使用指定的数据库URI(统一资源标识符)链接数据库服务器;Flask-SQLAlchemy中,数据库的URI通过配置变量SQLALCHEMY_DATABASE_URI设置,默认为SQLite内存内存型数据库(sqlite:///);SQLite是基于文件的DBMS,不需要设置数据库服务器,只需要指定数据库文件的绝对路径;
- SQLite数据库不限制文件后缀名,通常为foo.sqlite/foo.db/foo.sqlite3
- SQLAlCHEMY_TRACE_MODIFICATIONS配置变量决定是否跟踪对象的修改,用于Flask-SQLAlchemy的事件通知系统。这个配置键的默认值为None,如果没有特殊需要,可以设为False来关闭警告;
定义数据库模型
- 所有的模型类都要继承Flask-SQLAlchemy提供的db.Model基类,表的字段由db.Column()定义,字段的类型由Column的第一个参数传入,SQLAlchemy常见的字段类型:
- db.Integer
- db.String 可选参数Length用来设置最大长度
- db.Text 较长的Unicode文本
- db.Date 存储python的datetime.date对象
- db.Time datetime.time对象
- db.DateTime datetime.datetime对象
- db.Interval datetime.timedelta
- db.Float
- db.Boolean
- 默认情况下,Flask-SQLAlchemy会根据类名生成一个表名称,规则:
- Message -> message
- FooBar -> foo_bar
- 自定义表名称使用__tablename__属性来实现;
- 常用的SQLAlchemy字段参数:
- primary_key 设为True,该字段为主键;
- unique 设为True,该字段不允许出现重复值;
- index 设为True,为该字段创建索引,以提高查询效率;(不需要在所有列都建立索引,一般来说取值比较多的列以及经常被用来作为排序参照的列)
- nullable True表示可为空,默认为True
- default 为字段设置默认值
创建数据库和表
- db.create_all()
- 数据库和表一旦创建后,在模型类中添加或删除字段,修改字段的名称和类型等,再次调用create_all()也不会更新表结构。简单粗暴的方式:db.drop_all()方法删除数据库和表,然后再次创建;
- 在开发和部署后,经常需要在python shell中操作数据库,对于重复操作,可以编写成Flask命令、函数或是模型类的类方法;
数据库操作
SQLAlchemy使用数据库会话来管理数据库操作,这里的数据库会话也称为事务(事务的4大特性:原子性、一致性、隔离性、持久性),数据库中的会话代表一个临时存储区,你对数据库做出的改动都会存放在此,只有对数据库会话对象调用commit()方法时才被提交到数据库,这确保了数据提交的一致性。另外,会话也支持回滚操作,当你对会话调用rollback()方法时,添加到会话中且未提交的改动都将被撤销。
- CRUD - Create
- 实例化模型类作为一条记录;
- 添加新创建的记录到数据库会话:db.session.add()/db.session.add_all(列表)
- 提交数据库会话:db.session.commit()
- 主键由SQLAlchemy管理,模型类对象创建后作为临时对象,当提交数据库会话后,才会转化为记录写入数据库,这时模型类对象会自动获取id值;
- CRUD - Read
- 使用模型类提供的query属性附加调用各种过滤方法及查询方法可以完成这个任务:<模型类>.query.<过滤方法>.<查询方法>
- 常用的SQLAlchemy查询方法:
- get(id) 根据主键获取记录,未找到返回None;
- count() 返回查询结果的数量;
- one_or_none() 如果结果数量不为1,返回None;
- first_or_404() 返回第一条记录,找不到则返回404错误响应;
- get_or_404(id) 返回主键指定记录,找不到则返回404错误响应;
- paginate() 返回一个分页对象;
- with_parent(instance) 传入模型类实例作为参数,返回和这个实例相关联的对象;
- first_or_404()/get_or_404(id)/paginate()是Flask-SQLAlchemy附加的查询方法;
- 模型类的query属性存储的Query对象调用过滤方法会返回新的Query对象,因为每个过滤方法都会返回新的查询对象,多以过滤器可以叠加使用;
- 常用的SQLAlchemy过滤方法:
- filter() 使用指定的规则过滤记录
- filter_by() 使用关键字表达式的形式添加等值过滤过滤器,并且可以直接使用字段名称
- order_by() 根据指定条件对记录进行排序
- limit() 用指定的值限制原查询返回的记录数量
- group_by() 根据指定条件对记录进行分组
- offset() 使用指定的值偏移原查询的结果
- 直接打印查询对象或将其转化为字符串可以查看对应的SQL语句
- 关于过滤规方法中的过滤规则,常见的有:
- LIKE:filter(Note.body.like('%foo%'))
- IN:filter(Note.body.in_(['foo','bar']))
- NOT IN:filter(~Note.body.in_(['foo','bar']))
- AND:filter(_and(Note.body == 'foo', Note.title == 'Foo')) 或者 filter(Note.body == 'foo', Note.title == 'Foo') 或者 filter(Note.body == 'foo').filter(Note.title == 'Foo')
- OR:filter(_or(Note.body == 'foo', Note.title == 'Foo'))
- CRUD - Update
- 直接赋值给模型类的字段属性就可以改变字段值,然后调用commit()方法提交数据库;
- 只有要插入新的记录或将现有记录添加到会话中时才使用add()方法;
- CRUD - Delete
- 删除记录和添加记录很相似,不过要把add换成delete()方法,然后再调用commit方法;
在视图函数里操作数据库
- 如果表单字段的data属性不为空,WTForms会自动把data属性的值添加到表单字段的value属性中,作为表单的值填充进去;因此在渲染文章时,我们不用手动为<input>表单的value属性赋值。
- 防范CSRF攻击的基本原则就是正确使用GET和POST方法;像删除这类修改数据的操作绝对不能通过get请求来实现,正确的做法是为删除操作创建一个表单,这个表单只有1个提交字段,因为我们只需要在页面显示一个删除按钮来提交表单,渲染时不要忘了form.csrf_token字段;
- 如果提交表单且通过验证,就用get()方法查询对应记录,然后调用db.session.delete()方法删除并提交数据库会话。
- 表单的action属性需要设置为删除当前记录的URL,因为form表单提交的默认URL为当前页面的URL。
- 在HTML中,<a>标签会显示为链接,而提交按钮会显示为按钮,为了让编辑和删除的按钮显示相同样式,我们为这2个元素使用相同的CSS类。作为替代,可以考虑用JS创建监听函数,当删除按钮被按下时,提交对应的异常表单;
定义关系
在关系型数据库中,我们可以通过关系让不同表之间建立联系。定义关系分为2步:①创建外键;②建立联系,多对多关系中还需要定义关联表;
- 配置Python shell上下文
- 使用@app.shell_context_processor装饰器注册一个shell上下文处理函数,它和模板上下文处理函数一样,返回包含变量和变量值的字典。在启动flask shell时将在shell中自动导入字典中包含的变量;
- 一对多关系
- 定义外键:外键总是在多这一侧定义,用来存储一这一侧的id;如:author_id = db.Column(db.Integer, db.ForeignKey('author.id'))
- 定义关系属性:关系属性在关系的出发侧定义,即“一对多”的“一”这一侧;如:articles = db.relationship('Article');关系属性相当于一个快捷查询,不会被写入数据库中。使用了db.relationshape()定义关系属性,第一个参数为关系另一侧的模型名称,它会告诉SQLAlchemy将Author类与Article类建立关系。
- 建立关系有2种方式:
- 为外键字段赋值:spam.author_id=1
- 操作关系属性:集合关系属性可以像列表一样操作,调用append方法建立关系、remove方法解除关系:foo.articles.append(spam)
- 不要忘记在操作结束后调用commit提交数据库会话;
- 常用的SQLAlchemy关系函数参数(即db.relationshape参数):
- back_populates 反向引用,在关系另一侧也必须显示定义关系属性
- backref 反向引用,在关系另一侧自动创建关系属性
- lazy 指定如何加载相关记录
- uselist 指定是否使用列表加载相关记录,设为False则使用标量
- cascade 设置级联操作
- order_by 指定加载相关记录时的排序方式
- secondary 多对多关系中指定关联表
- primaryjoin 多对多关系中的一级级联条件
- secondaryjoin 多对多关系中的二级级联条件
- 当关系属性被调用时,关系函数会加载相应记录,控制记录加载方式的lazy参数常用选项:
- select 默认值,在必要时一次性加载相应的记录,返回包含记录的列表;
- joined 和父查询一样加载记录,但是用联结;
- immediate 一旦父查询加载就加载;
- subquery 类似于joined不过将用于子查询(不推荐,没有联结的效率高);
- dynamic 不直接加载记录,而是返回一个包含相关记录的query对象,以便继续添加查询函数对结果进行过滤;
- dynamic选项仅用于集合属性的关系,不可用于多对一、一对一或是在关系函数中将uselist参数设为False的情况;使用dynamic意味着每次操作都会执行一次SQL查询,这会造成潜在的性能问题。大多数情况下我们只需要使用默认值select即可,只有在调用关系属性会返回大量记录,并且总是需要对关系属性返回的结果附加额外查询时才需要动态加载(dynamic)。
- 建立双向关系
- 在关系2侧都添加关系属性来获取对方记录的关系我们称之为双向关系。
- 如在Writer和Book模型中分别添加如下关系属性:books = db.relationshape('Book', back_populates='writer');writer = db.relationshape('Writer', back_populates='books')
- 使用backref简化关系定义
使用关系函数中的backref参数可以简化双向关系的定义。以“一对多”关系为例,backref参数用来自动为关系另一侧添加关系属性,作为反向引用;
- 多对一
- 一对多关系反过来就是多对一关系;
- 在定义关系时,外键总是在多这一侧,所以在多对一关系中,外键和关系属性都放在多这一侧;
- 一对一
- 一对一关系实际上是通过建立一对多关系的基础上转换而来的;
- 在定义集合属性的关系函数中,将uselist属性设为False,这时一对多关系将被转换为一对一关系;
- 多对多
- 在一对多关系中,我们可以在多这一侧定义外键指向“一”这一侧,外键只能存储一个记录,但在多对多关系中,每一个记录都可以与关系另一侧的多个记录建立关系,关系2侧的模型都需要存储一组外键。在SQLAlchemy中,要想表示多对多关系,除了关系2侧的模型外,我们还需要创建一个关联表;关联表不存储数据,只用来存储关系2侧的外键对应关系;
- 借助关联表,我们可以把多对多关系转换成2个一对多关系;
- 关联表由SQLAlchemy接管,它会帮助我们管理这个表:我们只需要操作关系属性来建立和接触关系,SQLAlchemy会自动在关联表中创建或删除对应的关联表记录,而不用手动操作关联表;
更新数据库表
- 重新调用create_all()方法并不会起到更新表或重新创建表的作用。如果你并不在意表中的数据,最简单的方法是使用drop_all()方法删除表以及其中的数据,然后再调用create_all()方法重新创建表;
- 在使用@app.cli.command()注册命令时,可叠加@click.option('--drop', is_flag=True, help='Create after drop')添加一个--drop选项,将is_flag参数设为True可以将这个选项声明为布尔值标志。--drop选项的值作为参数drop传入命令函数,如果提供了这个选项,drop的值将是True,否则为False。
- click.confirm()函数可以添加一个确认提示,这样只有输入y或yes才会继续执行操作。
使用Flask-Migrate迁移数据库
- 数据库迁移工具可以在不破坏数据的情况下更新数据库表的结构;
- 扩展Flask-Migrate集成了Alembic,提供了一些Flask命令来简化迁移工作;
- pipenv install flask-migrate
- migrate = Migrate(app, db) 实例化Migrate类时,除了传入程序实例app,还需要传入实例化Flaks-SQLAlchemy提供的SQLAlchemy类创建的db对象作为第二个参数;
- 创建迁移环境
- flask db init
- Flask-Migrate提供了一个命令集,使用db作为命名集名称,它提供的命令以flask db开头;可以使用flask --help查看;
- 迁移环境只需要创建一次,将在项目根目录下创建一个migrations文件夹,其中包含了自动生成的配置文件和迁移版本文件夹;
- 生成迁移脚本
- flask db migrate -m "comment"
- 生成的迁移脚本主要包含了2个函数:
- upgrade()函数用来将改动应用到数据库;
- downgrade()函数用来撤销改动;
- Alembic自动生成的迁移脚本可能包含错误,所以有必要在生成后检查一下;
- Alembic为每一次迁移都生成了修订版本(revision)ID,所以数据库可以恢复到修改历史的任一点。正因如此,迁移环境中的文件也要纳入版本控制;
- 更新数据库
- flask db upgrade 如果没有创建新数据库表,这个命令会自动创建;如果已经创建,则会在不损坏数据库的前提下执行更新;
- flask db downgrade 执行回滚,撤销最后一次迁移在数据库中的改动;
- 开发环境时是否需要迁移
- 生产环境下:在生成自动迁移脚本后,执行更新之前,对迁移脚本进行检查,甚至是使用备份的数据库进行迁移测试,都是有必要的;
- 开发环境下:在开发环境下使用虚拟数据生成工具来生成虚拟数据,从而避免手动创建记录进行测试;另外本地开发时通常使用SQLite作为数据库引擎,SQLite不支持ALTER语句,而这正是迁移工具依赖的工作机制;
- 如果你想要让生产环境的部署更加高效,则应该尽可能让开发环境和生产环境保持一致。这时应该考虑直接在本地使用MySQL或PostgresSQL等性能更高的DBMS,然后设置迁移环境;
级联操作
Cascade意为级联操作,就是在操作一个对象的同时,对相关的对象也执行某些操作;级联行为通过关系函数relationshape()的cascade参数设置。设置了cascade参数的一侧将被视为父对象,相关的对象则被视为子对象;当没有设置cascade参数时,会使用默认值save-update、merge。常用的配置组合如下所示:
- save-update、merge
- save-update、merge、delete
- all 等同于除了delete-orphan外所有可用值的组合;
- all、delete-orphan
save-update
如果使用db.session.add()方法将一个对象添加到数据库会话,那么与其相关联的对象也将被添加到数据库会话;
delete
- 按照默认的行为,如果某个对象被删除,与之关联的另一个对象将与这个对象取消关联,外键字段的值将被清空。
- cascade参数设为delete时,当父对象被删除,与之关联的对象也将被一并删除;
- 当需要设置delete级联时,我们会将级联值设为all或save-update、merge、delete;
delete-orphan
- 这个模式是基于delete级联的,必须和delete级联一起使用,通常会设为all、delete-orpha;因为all包含delete。这个解除关系的对象被称为孤立对象(orpha object);
- 默认情况下父对象删除时,相关对象的外键会被设为空值。设置delete-orphan级联后,你会发现只要解除关系,子类对象就会被删除;
- delete和delete-orpha通常会在一对多关系中,而且“多”这一侧的对象附属于“一”这一侧的对象时使用。
- 对于这2个级联选项,如果你不会通过列表语义对集合关系属性调用remove()方法等方式来操作关系,那么使用delete级联即可;
事件监听
- 在Flask中,我们可以使用Flask提供的多个装饰器注册请求回调函数,它们会在特定的请求处理环节被执行。类似的,SQLAlchemy也提供了一个listen_for()装饰器,它可以用来注册事件回调函数;
- listen_for()装饰器主要接收2个参数,target参数表示监听对象,如模型类、类实例或类属性等;identifier参数表示被监听事件的标识符,如:用于监听属性的事件标识符有set/append/remove/init_scalar/init_collection等;
- 被注册的监听函数需要接收对应事件方法的所有参数,所以具体的监听函数用法因使用的事件而异;
- 除了接收所有事件方法接收的参数,还可以通过在listen_for()装饰器中将关键字参数named设为True,可以在监听函数中接收**kwargs作为参数,然后再函数中可以使用关键字参数从**kwargs字典获取对应的参数值;