Flask狼书笔记 | 05_数据库
5 数据库
这一章学习如何在Python中使用DBMS(数据库管理系统),来对数据库进行管理和操作。本书使用SQLite作为示例。
注:按下Ctrl+F5,或Shift+F5可以清除浏览器缓存。
5.1 数据库的分类
分为SQL(Structured Query Language)数据库和NoSQL(Not Only SQL)数据库。
- SQL:稍显复杂,但不容易出错,可以适应大部分场景。
-
NoSQL:灵活,效率高,可扩展性好等。
- 1、文档存储:使用类json格式来表示数据
- 2、键值对存储:通过键来存取数据,读写很快,常作为缓存使用。
5.2 ORM
ORM:Object-Relational Mapping,对象关系映射。
作用:
- 处理查询参数的转义,防止注入。
- 为不同的DBMS提供统一的接口。
- 能直接使用Python操作数据库,不需要写SQL语句。
ORM实现了三层映射关系:表 --> Python类,字段 --> 类属性,记录 --> 类实例。
# 定义表
from foo_orm import Model, Column, String
class Contact(Model):
__tablename__ = 'contacts'
name = Column(String(100), nullable=False)
# 插入记录
contact = Contact(name="zhang san")
5.3 使用Flask_SQLAlchemy
1、连接数据库:
首先连接数据库需要指定URI(Uniform Resource Identifier,统一资源标识符),URL(统一资源定位符)是它的子集。
常用的数据库URI格式:(p143)
SQLite是基于文件的DBMS,不需要数据库服务器,只需要指定数据库文件的绝对路径。配置数据库URI的代码如下:
from flask import Flask
import os
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
t = app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite:///' + os.path.join(app.root_path, 'data.db'))
db = SQLAlchemy(app)
print(t)
print(db)
运行输出:
sqlite:///D:code_allgitCodehelloflask_learn数据库data.db
<SQLAlchemy>
补充:
os.getenv
是一个Python标准库函数,它用于从环境变量中获取指定的值。这个函数的第一个参数是要查找的环境变量名称,第二个参数是默认值,如果未找到指定的环境变量,则返回这个默认值。
2、定义数据库模型:
模型类继承自SQLAlchemy提供的db.Model基类,表的字段由db.Column类的实例表示。
SQLAlchemy常用的字段类型:(p144)
常用的字段参数:(p145)
class Note(db.Model):
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.Text)
-
表名称会根据模型的类名称自动生成,可用
__tablename__
属性指定。 -
字段名默认为类属性名,看用
name
关键字参数指定。
3、创建数据库和表:
db.create_all()
可以查看模型对象的建表SQL语句:
from app import Note
from sqlalchemy.schema import CreateTable
print(CreateTable(Note.__table__))
改动模型类后,再次调用create_all()
不会更新表结构。可以调用drop_all()
方法删除数据库和表,然后重建。
可以自定义一个flaks命令完成数据库的创建工作,对于sqlite创建成功后会生成一个数据库文件,如data.db
。
import click
@app.cli.command()
def initdb():
db.create_all()
click.echo('Initialized database')
5.4 数据库操作
SQLAlchemy使用数据库会话(也称为事务)来管理数据库操作,会话代表一个临时存储区,对会话对象调用commit()
方法时,改动才被提交到数据库。调用rollback()
方法可以撤销会话中未提交的改动。
1、CRUD:
即Create、Read、Update、Delete。
- Create
note = Note(body='hello, world')
db.session.add(note)
db.session.commit()
通过add_all()
可以一次提交一个列表。
- Read
<模型类>.query.<过滤方法>.<查询方法>
Note.query.filter(Note.body=='hello, world').first
Query对象调用过滤方法的返回值仍然是一个Query对象,就像SQL的操作对象和返回结果都是表。查询方法返回的是模型类实例。
常用的SQLAchemy查询方法:(p148)
all()
,first()
,count()
,paginate()
,get(ident)
等常用过滤方法:(p150)
filter()
,filter_by()
,order_by()
,limit()
,group_by()
等常用查询操作符:(p150) LIKE,IN,NOT IN,AND,OR等。
- Update
note = Note.query.get(2)
note.body = 'hello, flask'
db.session.commit()
- Delete
note = Note.query.get(2)
db.session.delete(note)
db.session.commit()
2、在视图函数里操作数据库:
与在python shell中基本一致。
5.5 定义关系
1、配置Python Shell上下文
使用app.shell_context_processor
装饰器注册一个shell上下文处理函数,返回包含变量和变量值的字典。
@app.shell_context_processor
def make_shell_context():
return dict(db=db, Note=Note)
2、一对多关系
定义一对多关系包含两个部分:定义外键,和定义关系属性。其中关系属性相当于一个快捷查询,不会作为字段被写入到数据库中。下面的关系属性articles
会返回该作者所有文章的记录列表。
class Author(db.Model):
...
articles = db.relationship('Article')
class Article(db.Model):
...
author_id = db.Column(db.Integer, db.ForeignKey('athor.id'))
可以在两侧都定义一个关系属性,称为双向关系,需要用到back_populates
关键字参数,值为另一侧的关系属性名。
class Author(db.Model):
...
articles = db.relationship('Article', back_populates='author')
class Article(db.Model):
...
author_id = db.Column(db.Integer, db.ForeignKey('athor.id'))
author = db.relationship('Author', back_populates='articles')
可以使用
backref
简化关系定义,(p163)疑惑:为什么要手动在两侧都指定反向引用,而不是添加了外键属性之后就自动的呢?是采用了数据库中的索引吗?
定义关系后,建立关系有两种方式,一种是为外键字段赋值,另一种是通过操作关系属性。
# 为外键字段赋值
article_A.author_id = 1
# 操作关系属性: append, remove, pop
Mike.articles.append(article_A)
常用关系函数参数:p161
常用关系记录加载方式:p161
3、一对一关系
实际上是在通过建立一对多关系的双向关系的基础上转化而来,只是在原来”一“的一方设置userlist=False
,将集合属性变为标量属性。此后,无法再使用列表语义操作,如append
方法。
class Contry(db.Model):
...
capital = db.relationship('Capital', uselist=False)
class Capital(db.Model):
...
contry_id = db.Column(db.Integer, db.ForeignKey('country.id'))
contry = db.relationship('Country')
4、多对多关系
一对多关系中在“多”的一方存放外键,则“多”一方的每条记录只能有一条关系。我们可以单独创建一个关联表(db.Table
)来存储外键,表示多对多关系。使用secondary
参数来指定关联表。
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):
...
teachers = db.relationship('Teacher', secondary=association_table, back_populates='students')
class Teacher(db.Model):
...
students = db.relationship('Student', secondary=association_table, back_populates='teachers')
5.6 更新数据库表
1、重新生成表
方法很简单,但缺点是会丢失原来的所有数据。
db.drop_all()
db.create_all()
2、使用Flask-Migrate迁移(p169)
可以保留数据库中原有的数据。自动生成的迁移命令不一定可靠,必要时检查一下。
flask db init # 创建迁移环境
flask db migrate # 生成迁移脚本
flask db upgrade # 应用迁移
flask db downgrade # 撤销一次迁移
5.7 数据库进阶
1、级联操作(p172)
在relationship
方法可以配置cascade
参数,所有可用值为save-update,merge,refresh-expire,expunge,delete。默认值为save-update,merge
。
class Post(db.Model):
...
comments = db.relationship(..., cascade='save-update, merge, delete')
-
save-update:
db.session.add()
将Post对象添加到数据库会话时,相关的Comment对象也会被添加到数据库会话。 -
delete:Post记录被删除时,相关的Comment记录也会被删除。
-
delete-orphan:Post与Comment记录解除关系操作时,相应的Comment记录会被删除。
-
all:包含除了
delete-orphan
之外的所有可用值。
2、事件监听(p176)
在Flask中有请求回调函数,而SQLAlchemy也提供了listens_for()
装饰器来注册事件回调函数。装饰器接受两个参数,target
表示监听的对象,identifier
表示被监听事件的类型。被注册的监听函数需要接收对应事件方法的所有参数。
疑惑:“事件方法”指什么?怎么知道它有哪些参数?
class Draft(db.Model):
...
edit_time = ...
@db.event.listens_for(Draft.body, 'set', named=True)
def increment_edit_time(**kwargs):
if kwargs['target'].edit_time is not None:
kwargs['target'].edit_time += 1
设置named
参数为True可以使用kwargs
接收所有参数(不知道为啥)。kwargs
中的target
参数表示触发事件的模型类实例。
小结
SQLAlchemy入门教程:(p177)http://docs.sqlalchemy.org/en/latest/orm/tutorial.html
数据库这一章我看得有点拖拉,正值开学,可能需要好些天才能找回学习状态。大部分东西我还是之前都有所了解,因此看得比较流畅。在最近开发自己的玩具程序的过程中,数据库这一环节可给我制造了不少麻烦(特别是配环境),它在我眼里的黑盒程度又比较高,书中说到本章只是一个简单的介绍,不过暂时于我也够用了。