数据库是大多数动态Web程序的基础设施,本章主要介绍如何给Flask程序添加数据库支持,具体来说就是在Python中使用DBMS来对数据库进行管理和操作。
使用ORM不光可以解决SQL注入的问题,而且它为不同的DBMS提供统一的Python接口库,使得切换数据库非常简单。ORM把底层的SQL数据实体转化成高层的Python对象,这样甚至不用了解SQL,只需要通过Python代码即可完成数据库操作,ORM主要实现了三层映射关系:
SQLAlchemy是python社区使用最广泛的ORM之一,Flask-SQLAlchemy库集成了SQLAlchemy。要连接数据库服务器,首先要为我们的程序指定数据库URI。数据库的URI配置变量SQLALCHEMY_DATABASE_URI设置。
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os
app = Flask(__name__)
db = SQLAlchemy(app)
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', prefix + os.path.join(app.root_path, 'data.db'))
用来映射到数据库表的Python类被称为数据库模型,一个数据库模型类对应数据库中的一个表。所有模型类都需要继承Flask-SQLAlchemy提供的db.Model基类。
# Models
class Note(db.Model):
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.Text)
默认情况下,Flask-SQLAlchemy会根据模型类的名称生成一个表名称,类名称是单个单词时,就转换为小写,如果类名称是多个单词,就转化小写后用下划线分割。
在实例化字段时,通过把参数primary_key设为True可以将其定义为主键。主键是每一条记录(行)独一无二的标识,也是模型类中必须定义的字段。
添加一条新记录到数据库主要分三步:
1)创建Python对象(实例化模型类)作为一条记录
2)添加新创建的记录到数据库会话
3)提交数据库会话
note1 = Note(body='hello world')
db.session.add(note1)
db.session.commit()
id字段的数据没有定义是因为主键由SQLAlchemy管理,当提交数据库会话后,会自动获得id值。
一般来说,一个完整的查询遵循下面的模式:
<模型类>.query.<过滤方法>.<查询方法>
常用的SQLAlchemy查询方法
查询方法 | 说明 |
---|---|
all() | 返回包含所有查询记录的列表 |
first() | 返回查询的第一条记录,如果未找到,则返回None |
one() | 返回第一条记录,且仅允许有一条记录。若果记录数量大于等于1则报错 |
get(id) | 传入主键作为参数,返回指定主键值的记录 |
count() | 返回查询结果数量 |
one_or_none() | 类似one(),如果数量不为1,返回None |
first_or_404() | 返回查询的第一条记录,如果未找到,返回404错误 |
get_or_404(id) | 传入主键作为参数,返回指定主键值的记录,未找到报404错误 |
常用的SQLAlchemy过滤方法
过滤方法 | 说明 |
---|---|
filter() | 使用指定的规则记录,返回新产生的查询对象 |
filter_by() | 使用指定规则过滤记录(直接使用字段名称),返回新产生的查询对象 |
order_by() | 根据指定条件对记录进行排序,返回新产生的查询对象 |
limit(limit) | 使用指定的值限制原查询返回的记录数量,返回新产生的查询对象 |
group_by() | 根据指定条件对记录进行分组,返回新产生的查询对象 |
offset(offset) | 使用指定的值偏移原查询结果,返回新产生的查询对象 |
在filter()中除了"==“和”!="其他操作符如下:
filetr(Note.body.like('%foo%'))
filetr(Note.body.in_(['foo','bar','baz']))
filetr(~Note.body.in_(['foo','bar','baz']))
filetr(and_(Note.body=='foo%',Note.title=='Foobar'))
filetr(or_(Note.body=='foo%',Note.body=='bar'))
更新记录非常简单,直接赋值给模型类的字段属性就可以改变字段值,然后调用commit()方法提交即可。
note1.body = 'ni hao'
db.session.commit()
删除记录和添加记录很类似,不过要把add换成delete,最后需要commit提交修改。
db.session.delete(note1)
db.session.commit()
在关系型数据库中,我们可以通过关系让不同表之间的字段建立联系。一般来说,定义关系需要两步,分别是创建外键和定义关系属性。
例如作者和文章是一对多关系:
class Author(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20), unique=True)
phone = db.Column(db.String(20))
articles = db.relationship('Article') # collection
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)
author_id = db.Column(db.Integer, db.ForeignKey('author.id'))
定义关系的第一步是创建外键,外键是用来在A表存储B表的主键值以便和B表建立联系的关系字段。因为外键只能存储单一数据,所以外键总是在“多”这一侧定义。所以上述示例中我们需要为每篇文章添加外键存储作者的主键值以指向对应的作者。
class Article(db.Model):
...
author_id = db.Column(db.Integer, db.ForeignKey('author.id'))
在Article模型中,我们定义一个autho_id字段作为外键。这个字段使用db.ForeignKey类定义外键,传入关系另一侧的表名和主键字段名,即author.id。
定义关系的第二步是使用关系函数定义关系属性。关系属性在关系的出发侧定义,即一对多关系的“一”这一侧。在Author模型中,我们定义了一个articles属性来表示对应的多篇文章:
class Author(db.Model):
...
articles = db.relationship('Article')
relationship函数的第一个参数为关系另一侧的模型名称,它会告诉SQLAlchemy将Author类与Article类建立关系。当这个关系属性被调用时,SQLAlchemy会找到关系另一侧(即article表)的外键字段(即author_id)然后反向查询article表中所有author_id值为当前表主键值(即author.id)的记录,返回包含这些记录的列表,也就是返回某个作者对应的多篇文章记录。
关系属性定义完,下面我们为实际的对象建立关系。
首先建立一个作者记录和两个文章记录:
foo = Author(name='Foo')
spam = Article(title='Spam')
ham = Article(title='Ham')
建立关系,可以像列表一样操作,调用append方法来与一个article对象建立联系。
foo.article.append(spam)
foo.article.append(ham)
db.session.commit()
以上关系是单向的,还可以创建双向关系,也就是说在作者和文章类中都创建一个relationship函数,就可以在两个表间建立双向关系。
class Writer(db.Model):
...
books = db.relationship('Book',back_populates='writer')
class Book(db.Model):
...
writer = db.relationship('Writer',back_populates='books')
在关系函数中,我们使用back_populates参数来连接对方,back_populates参数的值需要设为关系另一侧的关系属性名。或者使用backref简化关系定义,backref参数用来自动为关系另一侧添加关系属性,作为反向引用。这时我们只需定义一个关系函数,但在使用上和上面定义两个关系函数效果完全相同。
class Writer(db.Model):
...
books = db.relationship('Book',backref='writer')
class Book(db.Model):
...
例如居民和城市的关系,前面说过关系属性在关系模式的出发侧定义,外键在“多”这一侧定义,所以在多对一关系中外键和关系属性都定义在“多”这一侧。
class Citizen(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(70), unique=True)
city_id = db.Column(db.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)
一对一关系实际上是通过建立双向关系的一对多关系的基础上转化来的。我们要确保关系两侧的关系属性都只返回单个值,所以要在定义集合属性的关系函数中将uselist参数设为false,这时一对多关系将被转化为一对一关系。以国家和首都为例。
class Country(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30), unique=True)
capital = db.relationship('Capital', back_populates='country', 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', back_populates='capital')
多对多关系中,关系两侧的模型都需要存储一组外键,因此我们创建一个关联表,关联表不存储数据,只用来存储关系两侧模型的外键对应关系。我们以老师和学生为例。
关联表使用db.Table类定义,传入的第一个参数是关联表名称。
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') # collection
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') # collection
借助关联表这个存储外键对的中间表,我们可以把多对多关系分化成两个一对多关系,即老师表和学生表相对于关联表都是一对多关系。
模型类(表)不是一成不变的,当添加了新的模型类,或是在模型类中添加了新的字段,甚至是修改了字段的名称或类型,都需要更新表。数据库表并不会随着模型的修改而自动更新。
最简单的方式就是删除所有表以及表中的数据,然后使用create_all()重新创建。但这种方式有很大的局限性,我们还可以借助数据库迁移工具(flask-migrate)来完成这个工作。数据库迁移工具可以在不破坏数据的情况下,更新数据库表的结构。
首先实例化Migrate类
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
$ flask db init
迁移环境只需要创建一次,这会在项目根目录下创建一个migrations文件夹,其中自动包含了自动生成的配置文件和迁移版本文件夹。
使用migrate子命令可以自动生成迁移脚本
$ flask db migrate -m "add note timestamp"
-m 选项用来添加迁移备注信息。
生成迁移脚本后,使用upgrade子命令即可更新数据库
$ flask db upgrade
参考资料:《Flask Web开发实战入门、进阶与原理解析》李辉 著