Flask之数据库
前言:本博客仅作记录学习使用,部分图片出自网络,如有侵犯您的权益,请联系删除
目录
一、数据库的分类
1.1、SQL
1.2、NoSQL
1.3、如何选择?
二、ORM魔法
三、使用Flask-SQLALchemy管理数据库
3.1、连接数据库服务器
3.2、定义数据库模型
3.3、创建数据库和表
四、数据库操作
4.1、CRUD
4.2、在视图函数里操作数据库
五、定义关系
5.1、配置Python Shell上下文
5.2、一对多
5.3、多对一
5.4、一对一
5.5、多对多
六、更新数据库表
6.1、重新生成表
6.2、使用Flask-Migrate迁移数据库
6.3、开发时是否要迁移?
七、数据库进阶实践
7.1、级联操作
7.2、事件监听
致谢
数据库是大多数动态Web程序的基础设施。常见的数据库管理系统(DBMS)有:MySQL、PostgreSQL、SQLite、MongoDB等。
一、数据库的分类
数据库一般分为两种,SQL(Structured Query Language,结构化查询语言)数据库和NoSQL(Not Only SQL,泛指非关系型)数据库
1.1、SQL
SQL数据库指关系型数据库,常用的SQL DBMS主要包括SQL Server、Oracle、MySQL、PostgreSQL、SQLite等。关系型数据库使用表来定义数据对象,不同的表之间使用关系连接。
| id | name | sex | occupation |
|---|---|---|---|
| 1 | Nick | Male | Journalist |
| 2 | Amy | Female | Writer |
在SQL数据库中,每一行代表一条记录(record),每条记录又由不同的列(column)组成。在存储数据前,需要预先定义表模式(schema),以定义表的结构并限定列的输入数据类型。
基本概念:
- 表(table):存储数据的特定结构
- 模式(schema):定义表的结构信息
- 列/字段(column/field):表中的列,存储一系列特定的数据,列组成表
- 行/记录(raw/record):表中的行,代表一条记录
-
标量(scalar):指的是单一数据,与之相对的是集合(collection)
1.2、NoSQL
NoSQL是初指No SQL或No Relational,现在NoSQL社区一般会解释为Not Only SQL。NoSQL数据库泛指不使用传统关系型数据库中的表格形式的数据库。近年来,NoSQL数据库越来越流行,被大量应用在实时Web程序和大型程序中。在速度和可扩展性方面有很大优势,除此之外还拥有无模式、分布式、水平伸缩等特点
最常用的两种NoSQL数据库如下:
1.2.1、文档存储(document store)
文档存储是NoSQL数据库中最流行的种类,它可作为主数据库使用。文档存储使用的文档类似SQL数据库中的记录,文档使用类JSON格式来表示数据。常见的文档存储DBMS有MongoDB、CouchDB等。1.1的身份信息表中的第一条记录使用文档可表示为:
{ id: 1, name: "Nick", sex: "Male", occupation: "Journalist" }1.2.2、键值对存储(key-value store)
键值对存储在形态上类似Python中的字典,通过键来存取数据,在读取上非常快,通常用来存储临时内容,作为缓存使用。常见的键值对DBMS有Redis、Riak等,其中Redis不仅可以管理键值对数据库,还可以作为缓存后端(cache backed)、图存储(graph store)等类型的NoSQL数据库。
1.3、如何选择?
- NoSQL 数据库不需要定义表和列等结构,也不限定存储的数据格式,在存储方式上比较灵活,在特定的场景下效率更高。
- SQL 数据库稍显复杂,但不容易出错, 能够适应大部分的应用场景
- 大型项目通常会同时需要多种数据库,比如使用MySQL作为主数据库存储用户资料和文章,使用Redis缓存数据,使用MongoDB存储实时消息。
大多情况,SQL数据库都能满足你的需求。为便于测试,我们使用SQLite作为DBMS。
二、ORM魔法
在Web应用程序里使用原生SQL语句操作数据库主要存在以下问题:
- 手动编写SQL语句比较乏味,而且视图函数中加入太多SQL语句会降低代码的易读性。另外还会有安全问题,如SQL语句注入
- 常见的开发模式是在开发时使用简单的SQLite,而在部署时切换到MySQL等更健壮的DBMS。但是对于不同DBMS需要使用不同的Python接口库,这让DBMS的切换变得不太容易。
ORM会自动处理参数的转义,尽可能地避免SQL注入的发生。另外还为不同的DBMS提供统一的接口,让切换工作变得简单。ORM扮演翻译的角色,将我们的Python语言转换为DBMS能够读懂的SQL指令,让我们能够使用Python来操控数据库。
ORM把底层的SQL数据库实体转化成高层的Python对象。ORM主要实现了三层映射关系:
- 表--Python类
- 字段(列)--类属性
- 记录(行)--类实例
比如,创建一个contacts表来存储留言,其中包含用户名称和电话号码两个字段。在SQL中:
CREATE TABLE contacts( name varchar(100) NOT NULL, phone_number varchar(32), )
如果使用ORM:
from foo_orm import Model, Column, String class Contact(Model): __tablename__ = 'contacts' name = Column(String(100),nullable=False) phone_number = Column(String(32))
要向表中插入一条记录,需要使用下面的SQL语句:
INSERT INTO contacts(name,phone_number) VALUES('Grey Li','12345678')使用ORM则只需要创建一个Contact类的实例,传入对应的参数表示各个列的数据即可。
contact = Contact(name="Grey Li",phone_number="12345678")
除了便于使用,ORM还有下面这些优点:
- 灵活性好。既能使用高层对象来操作数据库,又支持执行原生SQL语句。
- 提升效率。从高层对象转换成原生SQL会牺牲一些性能,但这换取的是巨大的效率提升
- 可移植性好。ORM通常支持多种DBMS,只需要稍微改动少量配置
使用Python实现的ORM有SQLALchemy、Peewee、PonyORM等。其中SQLALchemy是Python社区使用最广泛的ORM之一
三、使用Flask-SQLALchemy管理数据库
扩展Flask-SQLALchemy集成了SQLALchemy,它简化了连接数据库服务器、管理数据库操作会话等各类工作,让Flask中的数据处理体验变得更加轻松。
pip install flask-sqlalchemy
实例化Flask-SQLALchemy提供的SQLALchemy类,传入 程序实例app以完成扩展的初始化:
from flask import Flask from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) db = SQLAlchemy(app)
3.1、连接数据库服务器
DBMS通常会提供数据库服务器运行在操作系统中。要连接数据库服务器,首先要为我们的程序指定数据库URI(Uniform Resource Identifier,统一资源标识符)。数据库URI是一串包含各种属性的字符串,其中包含了各种用于连接数据库的信息。
常用的数据库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: 在Flask-SQLALchemy中,数据库的URI通过配置变量SQLALCHEMY_DATABASE_URI设置,默认为SQLite内存型数据库(sqlite:///:memory:),SQLite是基于文件的DBMS,不需要设置数据库服务器,只需要指定数据库文件的绝对路径。我们使用app.root_path来定位数据库文件的路径,并将数据库文件命名为data.db:
import os app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL','sqlite:///'+os.path.join(app.root_path,'data.db'))在生产环境下更换到其他类型的DBMS时,数据库URL会包含敏感信息,所有这里优先从环境变量DATABSE_URL获取。
SQLite的数据库URI在Linux或macOS系统下的斜线数量是4个;在Windows系统下的URI中的斜线数量为3个。内存型数据库的斜线固定为3个。文件名不限后缀,常用的命名方式有foo.sqlite,foo.db或是注明版本的foo.sqlite3
设置好数据库URI后,在Python Shell中导入并查看db对象会得到下面的输出
>>> from app import db >>> db
3.2、定义数据库模型
用来映射到数据库表的Python类通常被称为数据库模型(model),一个数据库模型类对应数据库中的一个表。定义模型即使用Python类定义表模式,并声明映射关系。所有模型类都需要继承Flask-SQLALchemy提供的db.Model基类。下面定义一个Note模型类,用来存储笔记:
class Note(db.Model): id = db.Column(db.Integer,primary_key=True) body = db.Column(db.Text)表的字段(列)由db.Column类的实例表示,字段的类型通过Column类构造方法的第一个参数传入。常用的SQLALchemy字段类型:
字段 说明 Integer 参数 String 字符串,可选参数length可以用来设置最大长度 Text 较长的Unicode文本 Date 日期,存储Python的datetime.date对象 Time 时间,存储Python的datetime.time对象 DateTime 时间和日期,存储Python的datetime对象 Interval 时间间隔,存储Python的datetime.timedelta对象 Float 浮点数 Boolen 布尔值 PickleType 存储Pickle列化的Python对象 LargeBinary 存储任意二进制数据 字段类型一般直接声明即可,如果需要传入参数,也可以添加括号。对于类似String的字符串列,有些数据库会要求限定长度,因此最好为其指定长度。虽然使用Text类型可以存储相对灵活的变长文本,但从性能上考虑,我们仅在必须的情况下使用Text类型,比如用户发表的文章和评论等不限长度的内容。
一般情况字段的长度是由程序设计者自定的。但也有特殊约束:比如姓名(英语)的长度一般不超过70个字符,中文名一般不超过20个字符,电子邮件地址的长度不超过254个字符。(当在数据库模型类中限制了字段的长度后,在接收对应数据的表单类字段里,也需要使用Length验证器来验证用户的输入数据)。
默认情况下,Flask-SQLALchemy会限制模型类的名称生成一个表名称,生成规则如下:
Message --> message # 单个单词转换为小写 FooBar --> foo_bar # 多个单词转换为小写并使用下划线分隔
Note类对应的表名称即note。若想自己指定名称,可以通过定义tablename属性来实现。字段名默认为类属性名,也可以通过字段类构造方法的第一个参数指定,或使用关键字name。根据我们定义的Note模型类,最终生成一个note表,表中包含id和body字段,
常用的SQLALchemy字段参数:
参数名 说明 primary_key 如果设为True,该字段为主键 unique 如果设为True,该字段不允许出现重复值 index 如果设为True,为该字段创建索引,以提高查询效率 nullable 确定字段值可否为空,值为True或False,默认值为True default 为字段设置默认值 (不需要在所有列都建立索引。一般来说,取值可能性多(比如姓名)的列,以及经常被用来作为排序参照的列(比如时间戳)更适合建立索引。)
3.3、创建数据库和表
创建模型类后,我们需要手动创建数据库和对应的表,也就是我们常说的建库和建表。这通过我们的db对象调用create_all()方法实现。
$ flask shell >>> from app import db >>> db.create_all()
如果将模型类定义在单独的模块中,那么必须在调用db.create_all()方法前导入相应模块,以便让SQLALchemy获取模型类被创建时生成的表信息,进而正确生成数据表。
通过下面的方式可以查看模型对应的SQL模式(建表语句):
>>> from sqlalchemy.schema import CreateTable >>> print(CreateTable(Note.__table__)) CREATE TABLE note ( id INTEGER NOT NULL, body TEXT, PRIMARY KEY (id) )(我们数据库和表一旦创建后,之后对模型的改动不会自动作用到实际的表中。如要使改动生效,调用db.drop_all()方法删除数据库和表,然后再调用create_all()方法创建)。
我们也可以自定义flask命令完成这个工作:
import click @app.cli.command() def initdb(): db.create_all() click.echo('Initialized database')在命令行输入flask initdb即可创建数据库和表:
$ flask initdb Initialized database.
四、数据库操作
数据库操作主要是CRUD,即Create(创建)、Read(读取/查询)、Update(更新)和Delete(删除)。
SQLALchemy使用数据库会话来管理数据库操作,这里的数据库会话也称事务(transaction)。Flask-SQLALchemy自动帮我们创建会话,可以通过db.session属性获取
数据库中的会话代表一个临时缓存区,对数据库做出的任何改动都会存放在这里。可以调用add()方法将新创建的对象添加到数据库会话中,或是对会话中的对象进行更新。只有当你对数据库会话对象调用commit()方法时,改动才会被提交到数据库,这确保了数据提交的一致性。另外数据库会话也支持回滚操作。当你对会话调用rollback()方法时,添加到会话中且未提交的改动都将被撤销。
4.1、CRUD
默认情况下,Flask-SQLALchemy会自动为模型生成一个__repr()方法。当在Python shell中调用模型的对象时,__reper()方法会返回一条类似“”的字符串,比如。为了便于操作,本示例重新定义__repr__()方法,返回一些更有用的信息。
class Note(db.Model): ... def __repr__(self): return '' % self.body4.1.1、Create
添加一条新记录到数据库主要分为三步:
- 创建Python对象(实例化模型类)作为一条记录。
- 添加新创建的记录到数据库会话
- 提交数据库会话
# 下面示例向数据库中添加了三条留言 >>> from app import db,Note >>> note1 = Note(body='remember Sammy Jankis') >>> note2 = Note(body='SHAVE') >>> note3 = Note(body='DON NOT 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()方法一次添加包含所有记录对象的列表。
我们在创建模型类的时候并没有定义id字段的数据,这是因为主键由SQLALchemy管理。模型类对象创建后作为临时对象(transient),当你提交数据库会话后,模型类对象才会转换为数据库记录写入数据库中,这时模型类对象会自动获得id值。
4.1.2、Read
使用模型类提供的query属性附加调用各种过滤方法及查询方法即可从数据库中取出数据
一般来说,一个完整的查询遵循下面的模式:
.query..
从某个模型出发,通过在query属性对应的Query对象上附加的过滤方法和查询函数对模型类对应的表中的记录进行各种筛选和调整,最终返回包含对应数据库记录数据的模型类实例,对返回的实例调用属性即可获得对应的字段数据。
SQLALchemy提供了许多查询方法用来获取记录:
查询方法 说明 all() 返回包含所有查询记录的列表 first() 返回查询的第一条记录,如果未找到,则返回None one() 返回第一条记录,且仅允许有一条记录。如果记录数量大于1或小于1,则抛出错误 get(ident) 传入主键值作为参数,返回指定主键值的记录,如果未找到,则返回None count() 返回查询结果的数量 one_or_none() 类似one(),如果结果数量不为1,返回None first_or_404() 返回查询的第一条记录,如果未找到,则返回404错误响应 get_or_404(ident) 传入主键值作为参数,返回指定主键值记录,如果未找到,则返回404错误响应 paginate() 返回一个Pagination对象,可以对记录进行分页处理 with_parent(instance) 传入模型实例作为参数,返回和这个实例相关联的对象,后面会详细介绍 示例:all()返回所有记录:
>>> Note.query.all() [, , ]
first()返回第一条记录:
>>> note1 = Note.query.first() >>> note1 >>> note1.body 'remember Sammy Jankis'
get()返回指定主键值(id字段)的记录:
>>> note2 = Note.query.get(2) >>> note2
count()返回记录的数量:
>>> Note.query.count() 3
SQLALchemy还提供许多过滤方法,使用这些过滤方法可以获取更精确的查询,比如获取指定字段值的记录。对模型类的query属性存储的Query对象调用过滤方法将返回一个更精确的Query对象(后面简称为查询对象)。因为每个过滤方法都会返回新的查询对象,所以过滤器可以叠加使用。
在查询对象上调用前面介绍的查询方法,即可获得一个包含过滤后的记录的列表。常用的过滤方法有:
查询过滤器的名称 说明 filter() 使用指定的规则过滤记录,返回新产生的查询对象 filter_by() 使用指定的规则过滤记录(以关键字表达式的形式),返回新产生的查询对象 order_by() 根据指定条件对记录进行排序,返回新产生的查询对象 limit(limit) 使用指定的值限制原查询返回的记录数量,返回新产生的查询对象 group_by() 根据指定条件对记录进行分组,返回新产生的查询对象 offset(offset) 使用指定的值偏移原查询的结果,返回新产生的查询对象 filter()方法是最基础的查询方法。它使用指定的规则来过滤记录,示例:在数据库中找出body字段值为"SHAVE"的记录:
>>> Note.query.filter(Note.body=='SHAVE').first()
直接打印查询对象或将其转换为字符串可查看对应的SQL语句:
>>> print(Note.query.filter_by(body='SHAVE')) SELECT note.id AS note_id, note.body AS note_body FROM note WHERE note.body = ?
在filter()方法中传入表达式时,除了"=="以及表示不等于的"!=",其他常用的查询操作符以及使用示例如下:
LIKE: filter(Note.body.like('%foo%')) IN: filter(Note.body.in_(['foo','bar','baz'])) NOT IN: filter(~Note.body.in_(['foo','bar','baz'])) AND: # 使用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: from sqlalchemy import or_ filter(or_(Note.body == 'foo',Note.body == 'bar'))和filter方法相比,filter_by()方法更易于使用。在filter_by()方法中,可以使用关键字表达式来指定过滤规则。更方便的是,可以在这个过滤器 中直接使用字段名字。
>>> Note.query.filter_by(body='SHAVE').first()
其他方法,后续使用时介绍。
4.1.3、Update
更新一条记录非常简单,直接赋值给模型类的字段属性就可以改变字段值,然后调用commit()方法提交给会话即可。示例:改变一条记录的body字段的值:
>>> note = Note.query.get(2) >>> note.body 'SHAVE' >>> note.body = 'SHAVE LEFT THING' >>> db.session.commit()
4.1.4、Delete
删除记录和添加记录很相似,不过要把add()方法换成delete()方法,最后都需要调用commit()方法提交修改。示例:删除id(主键)为2的记录:
>>> note = Note.query.get(2) >>> db.session.delete(note) >>> db.session.commit()
4.2、在视图函数里操作数据库
在视图函数里操作数据库的方式和我们在Python Shell中练习的大致相同,只不过需要一些额外的工作。比如把查询结果作为参数传入模板渲染出来,或是获取表单的字段值作为提交到数据库的数据。
4.2.1、Create
为了支持输入笔记内容,我们先创建一个用于填写新笔记的表单:
from wtforms import TextAreaField from flask_wtf import FlaskForm from wtforms.validators import DataRequired class NewNoteForm(FlaskForm): body = TextAreaField('Body',validators=[DataRequired()]) submit = SubmitField('Save')我们创建一个new_note视图,这个视图负责渲染创建笔记的模板,并处理表单的提交:
@app.route('/new',methods=['GET','POST']) def new_note(): form = NewNoteForm() if form.validate_on_submit(): body = form.body.data note = Note(body=body) db.session.add(note) db.session.commit() flash('Your note is saved.') return redirect(url_for('index')) return render_template('new_note.html',form=form)逻辑:当form.validate_on_submit()返回True时,即表单被提交且验证通过时,获取表单body字段的数据,然后创建新的Note实例,将表单中body字段的值作为body参数传入,最后添加到数据库会话中并提交会话。这个过程接收用户通过表单提交的数据并保存到数据库中,最后我们使用flash()函数发送提示消息并重定向到index视图。
表单在new_note.html模板中渲染,这里使用前面介绍的form_filed宏渲染表单字段,传入rows和cols参数来定制输入框的大小:
{% from 'macro.html' import form_field %} {% block content %}New Note
{{ form.csrf_token }} {{ form_field(form.body,rows=5,cols=50) }} {{ form.submit }} {% endblock %}index视图用来显示主页,目前它的所有作用就是渲染主页对应的模板:
@app.route('/') def index(): return render_template('index.html')在对应的index.html模板中,我们添加一个指向创建新笔记页面的链接:
Notebook
{ url_for('new_note') }}">New Note4.2.2、Read
上面为程序添加了新笔记的功能,当在创建笔记的页面单击保存后,程序会重定向到主页,提示的消息告诉你刚刚提交的笔记已经成功保存,可却无法看到保存后的笔记。为了在主页列出所有保存的笔记,我们需要修改index视图:
@app.route('/index') def index(): form = DeleteForm() notes = Note.query.all() return render_template('index.html',notes=notes,form=form)在模板中渲染数据库记录:
Notebook
{ url_for('new_note') }}">New Note{{ notes|length }} notes:
{% for note in notes %}{{ note.body }}
{% endfor %}在模板中,我们迭代这个notes列表,调用Note对象的body属性获取body字段的值。另外,我们还通过length过滤器获取笔记的数量。

4.2.3、Update
更新一条笔记和创建一条新笔记的实现代码几乎完全相同,首先是编辑笔记的表单:
class EditNoteForm(FlaskForm): body = TextAreaField('Body',validators=[DataRequired()]) submit = SubmitField('Update')发现这和创建新笔记NewNoteForm唯一的不同是提交字段的标签参数不同,因此这个表单的定义也可以通过继承来简化:
class EditNoteForm(NewNoteForm): submit = SubmitField('Update')用来渲染更新笔记页面和处理更新表单提交的edit_note视图:
@app.route('/edit/',methods=['GET','POST']) def edit_note(note_id): form = EditNoteForm() note = Note.query.get(note_id) if form.validate_on_submit(): note.body = form.body.data db.session.commit() flash('Your note is update.') return redirect(url_for('index')) form.body.data = note.body return render_template('edit_note.html',form=form)逻辑:通过URL变量note_id获取要修改的笔记的主键值(id字段),然后我们就可以使用get()方法获取对应的Note实例。当表单被提交且通过验证时,我们将表单中body字段的值赋值给note对象的body属性,然后提交数据库会话,这样就完成了更新操作。最后重定向。
注意,在GET请求的执行流程中,我们添加了这行代码:
form.body.data = note.body
因为要添加笔记内容的功能,那么当我们打开修改某个笔记的页面时,这个页面的表单中必然要包含笔记原有的内容。
如果手动创建HTML表单,那么可以通过将note记录传入模板,然后手动为对应字段填入笔记的原有内容:
{{ note.body }}其他input元素则通过value属性来设置输入框中的值:

