Skip to content

关系对象模型(ORM)

数据库是 Web 程序的基础设施,只要你想把数据存储下来,就离不开数据库。 这里提及的数据库(Database)指的是由存储数据的单个或多个文件组成的集合,它是一种容器,可以类比为文件柜。 而人们通常使用数据库来表示操作数据库的软件,这类管理数据库的软件被称为数据库管理系统(DBMS,Database Management System),常见的 DBMS 有 MySQL、PostgreSQL、SQLite 等。

SQLAlchemy

在 Web 应用里使用原生 SQL 语句操作数据库主要存在下面两类问题:

  • 手动编写 SQL 语句比较繁琐,视图函数中加入太多 SQL 语句会降低代码的易读性。还会容易出现安全问题,比如 SQL 注入。
  • 常见的开发模式是在开发时使用简单的 SQLite,而在部署时切换到 MySQL 等更健壮的 DBMS。但是对于不同的 DBMS,我们需要使用不同的 Python 接口库,这让 DBMS 的切换变得不太容易。

使用 ORM 可以避免 SQL 注入问题,但仍然需要对传入的查询参数进行验证。 在执行原生 SQL 语句时也要注意避免使用字符串拼接或字符串格式化的方式传入参数。

使用 ORM 可以很大程度上解决这些问题。它会自动帮你处理查询参数的转义,尽可能地避免 SQL 注入的发生。另外,它为不同的 DBMS 提供统 一的接口,让切换工作变得非常简单。ORM 扮演翻译的角色,能够将我们 的 Python 语言转换为 DBMS 能够读懂的 SQL 指令,让我们能够使用 Python 来操控数据库。

尽管 ORM 非常方便,但是自己编写 SQL 代码可以获得更大的灵活性和性能优势。

ORM 把底层的 SQL 数据实体转化成高层的 Python 对象,这样一来,你甚至不需要了解 SQL,只需要通过 Python 代码即可完成数据库操作,ORM 主要实现了三层映射关系:

  • 表 --> Python 类。
  • 字段(列) --> 类属性。
  • 记录(行) --> 类实例。


比如,我们要创建一个 Student 表来存储学生信息,其中包含学生名称和年龄两个字段。在 SQL 中,下面的代码用来创建这个表:

sql
CREATE TABLE students
(
    name varchar(100) NOT NULL,
    age  int
);

如果使用 ORM ,我们可以使用类似下面的 Python 类来定义这个表:

python
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base

# 创建对象的基类:
Base = declarative_base()


class Student(Base):
    __tablename__ = 'students'
    name = sa.Column(sa.String(100), nullable=False)
    age = sa.Column(sa.Integer)

要向表中插入一条记录,需要使用下面的 SQL 语句:

sql
INSERT INTO students(name, age)
VALUES ('zhengxin', 18);

使用 ORM 则只需要创建一个 Student 类的实例,传入对应的参数表示各个列的数据即可。下面的代码和使用上面的 SQL 语句效果相同:

python
student = Student(name='zhengxin', age=18)

除了便于使用,ORM 还有下面这些优点:

  • 灵活性好。你既能使用高层对象来操作数据库,又支持执行原生 SQL 语句。
  • 提升效率。从高层对象转换成原生 SQL 会牺牲一些性能,但这微不足道的性能牺牲换取的是巨大的效率提升。
  • 可移植性好。ORM 通常支持多种 DBMS,包括 MySQL、 PostgreSQL、Oracle、SQLite 等。你可以随意更换 DBMS ,只需要稍微改动少量配置。

使用 Python 实现的 ORM 有 SQLAlchemy、PonyORM 等。其中 SQLAlchemy 是 Python 社区使用最广泛的 ORM 之一

Flask-SQLAlchemy

扩展 Flask-SQLAlchemy 集成了SQLAlchemy,它简化了连接数据库服务器、管理数据库操作会话等各类工作,让Flask中的数据处理体验变得更加轻松。

shell
pip install flask-sqlalchemy

下面在示例程序中实例化 Flask-SQLAlchemy 提供的 SQLAlchemy 类, 传入程序实例 app,以完成扩展的初始化:

python
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
db = SQLAlchemy(app)

为了便于使用,可以把实例化扩展类的对象命名为 db。这个 db 对象代表数据库,它可以使用 Flask-SQLAlchemy 提供的所有功能。

虽然使用的大部分类和函数都由 SQLAlchemy 提供,但在 Flask-SQLAlchemy 中,大多数情况下,不需要手动从 SQLAlchemy 导入类或函数。 在 sqlalchemy 和 sqlalchemy.orm 模块中实现的类和函数,以及其他几个常用的模块和对象都可以作为 db 对象的属性调用。 当我们创建这样的调用时,Flask-SQLAlchemy 会自动把这些调用转发到对应的类、函数或模块。

连接数据库服务器

DBMS 通常会提供数据库服务器运行在操作系统中。要连接数据库服 务器,首先要为我们的程序指定数据库 URI(Uniform Resource Identifier,统一资源标识符)。数据库 URI 是一串包含各种属性的字符串,其中包含了各种用于连接数据库的信息。

URI 代表统一资源标识符,是用来标示资源的一组字符串。URL 是它的⼦集。在大多数情况下,这两者可以交替使用。 下表是一些常用的 DBMS 及其数据库 URI 格式示例。

DBMSURI
PostgreSQLpostgresql://username:password@host/databasename
MySQLmysql://username:password@host/databasename
Oracleoracle://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:

数据库的 URI 通过配置变量 SQLALCHEMY_DATABASE_URI 设置,默认为 SQLite 内存型数据库 (sqlite:///:memory:)。SQLite 是基于文件的 DBMS,所以只需要指定数据库文件的绝对路径。

python
from flask import Flask
# 1. 导入 flask-sql alchemy 中的 SQLAlchemy 对象
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)


class Config:
    # 数据库链接配置参数
    SQLALCHEMY_DATABASE_URI = 'sqlite:///data.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = False


# 2. 提前加载数据库配置
app.config.from_object(Config)

# 3. 创建数据库链接对象
db = SQLAlchemy()

# 4. 将数据库操作对象与 flask app 进行绑定
db.init_app(app)

注意

SQLite 的数据库 URI 在 Linux 或 macOS 系统下的斜线数量是4个;在 Windows 系统下的 URI 中的斜线数量为3个。内存型数据库的斜线固定为 3 个。

SQLite 数据库文件名不限定后缀,常用的命名方式有 foo.sqlite, foo.db,或是注明 SQLite 版本的 foo.sqlite3。

设置好数据库 URI 后,在 Python Shell 中导入并查看 db 对象会获得下面 的输出:

>>> from app import db
>>> db
<SQLAlchemy engine = sqlite:///Path/to/your/data.db>

安装并初始化 Flask-SQLAlchemy 后,启动程序时会看到命令行下有一行警告信息。这是因为 Flask-SQLAlchemy 建议你设置 SQLALCHEMY_TRACK_MODIFICATIONS 配置变量,这个配置变量决定是否追踪对象的修改,这用于 Flask-SQLAlchemy 的事件通知系统。这个配置键的默认值为 None,如果没有特殊需要,我们可以把它设为 False 来关闭警告信息:

python
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

定义数据库模型

用来映射到数据库表的 Python 类通常被称为数据库模型(model),一 个数据库模型类对应数据库中的一个表。定义模型即使用Python类定义表模式,并声明映射关系。所有的模型类都需要继承 Flask-SQLAlchemy 提供 的 db.Model 基类。

本章的示例程序是一个笔记程序,笔记保存到数据库中,你可以通过程序查询、添加、更新和删除笔记。我们定义一个 Student 模型类,用来存储笔记。

python
# 5. 创建数据库模型
class Student(db.Model):
    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.String(length=20))
    math = sa.Column(sa.Integer)
    chinese = sa.Column(sa.Integer)
    english = sa.Column(sa.Integer)

在上面的模型类中,表的字段(列)由 db.Column 类的实例表示,字段 的类型通过 Column 类构造方法的第一个参数传入。在这个模型中,我们创 建了一个类型为 sa.Integer 的 id 字段和类型为 sa.Text 的 body 列,分别存储整 型和文本。常用的 SQLAlchemy 字段类型如下表所示。

SQLAlchemy 常用的字段类型

Integer整数
String字符串,可选参数length可以用来设置最大长度
Text较长的Unicode文本
Date日期,存储Python的datetime.date对象
Time时间,存储Python的datetime.time对象
DateTime时间和日期,存储Python的datetime对象
Interval时间间隔,存储Python的datetime.timedelta对象
Float浮点数
Boolean布尔值
PickleType存储Pickle列化的Python对象
LargeBinary存储任意二进制数据

字段类型一般直接声明即可,如果需要传入参数,可以添加括号。对于类似 String 的字符串列,有些数据库会要求限定长度,因此最好为其指定长度。 虽然使用 Text 类型可以存储相对灵活的变长文本,但从性能上考虑,我们仅在必须的情况下使用 Text 类型,比如用户发表的文章和评论等不限长度的内容。

注意当你在数据库模型类中限制了字段的长度后,在接收对应数据的表单类字段里,也需要使用 Length 验证器来验证用户的输入数据。 默认情况下,Flask-SQLAlchemy 会根据模型类的名称生成一个表名称,生成规则如下:

Message --> message # 单个单词转换为⼩写 
FooBar --> foo_bar # 多个单词转换为⼩写并使用下划线分隔

Student 类对应的表名称即 student。如果你想自己指定表名称,可以通过定义 __tablename__ 属性来实现。字段名默认为类属性名,你也可以通过字段 类构造方法的第一个参数指定,或使用关键字 name。根据我们定义的 Student 模型类,最终将生成一个 Student 表,表中包含id和body字段。

除了 name 参数,实例化字段类时常用的字段参数如表所示。

常用的 SQLAlchemy 字段参数

参数名说 明
primary key如果设为 True,该字段为主键
unique如果设为 True,该字段不允许出现重复值
index如果设为 True,为该字段创建索引,以提高査询效率
nullable确定字段值可否为空,值为 True 或 False ,默认值为 True
default为字段设置默认值

在实例化字段类时,通过把参数 primary_key 设为 True 可以将其定义为主键。在我们定义的 Student 类中,id 字段即表的主键(primary key)。主键是每一条记录(行)独一无二的标识,也是模型类中必须定义的字段,一般命名为 id 或 pk。

创建数据库和表

如果把数据库(文件)看作一个仓库,为了方便取用,我们需要把货物按照类型分别放置在不同货架上,这些货架就是数据库中的表。 创建模型类后,我们需要手动创建数据库和对应的表,也就是我们常说的建库和建表。 这通过对 db 对象调用 create_all() 方法实现:

shell
$ flask shell 
>>> from app import db 
>>> db.create_all()

如果模型类定义在单独的模块中,那么必须在调用 db.create_all() 方法前导入相应模块,以便让 SQLAlchemy 获取模型类被创建时生成的表信息,进而正确生成数据表。

数据库和表一旦创建后,之后对模型的改动不会自动作用到实际的表中。比如,在模型类中添加或删除字段,修改字段的名称和类型,这时再次调用 create_all() 也不会更新表结构。如果要使改动生效,最简单的方式是调用 db.drop_all() 方法删除数据库和表,然后再调用 db.create_all() 方法创建,后面会具体介绍。

自定义用于创建数据库和表的 flask 命令

我们也可以自己实现一个自定义 flask 命令完成这个工作

python
import click


@app.cli.command()
def init():
    """创建数据库"""
    db.create_all()
    click.echo('初始化数据库。')

在命令行下输入 flask init 即可创建数据库和表:

shell
flask initdb

对于示例程序来说,这会在 instance 目录下创建一个 data.db 文件。

更新数据库表

模型类(表)不是一成不变的,当你添加了新的模型类,或是在模型 类中添加了新的字段,甚至是修改了字段的名称或类型,都需要更新表。 在前面我们把数据库表类比成盛放货物的货架,这些货架是固定生成的。 当我们在操控程序(DBMS/ORM)上变更了货架的结构时,仓库的货架也 要根据变化相应进行调整。而且,当货架的结构产生变动时,我们还需要 考虑如何处理货架上的货物(数据)。

当你在数据库的模型中添加了一个新的字段后,比如在 Student 模型里添加了一个创建时间的 create_at 字段。这时你可能想要立刻启动程 序看看效果,遗憾的是,你看到了下面的报错信息:

OperationalError: (sqlite3.OperationalError) no such column: student.create_at [...]

这段错误消息指出 student 表中没有 create_at 列,并在中括号里给出了查询所对应的 SQL 原语。之所以会出现这个错误,是因为数据库表并不会随 着模型的修改而自动更新。想想我们之前关于仓库的比喻,仓库里来了一批新类型的货物,可我们还没为它们安排相应的货架,这当然要出错了。 下面我们会学习如何更新数据库。

重新生成表

重新调用 create_all() 方法并不会起到更新表或重新创建表的作用。 如果你并不在意表中的数据,最简单的方法是使用 drop_all() 方法删除表 以及其中的数据,然后再使用 create_all() 方法重新创建:

python
>>> db.drop_all()
>>> db.create_all()

这会清除数据库里的原有数据,请勿在生产环境下使用。

为了方便开发,我们修改 init 命令函数的内容,为其增加一个 --drop 选项来支持删除表和数据库后进行重建

支持删除表后重建

python
@app.cli.command()
@click.option('--drop', is_flag=True, help='Create after drop.')
def init(drop):
    """Initialize the database."""
    if drop:
        click.confirm('This operation will delete the database, do you want to continue?', abort=True)
        db.drop_all()
        click.echo('Drop tables.')
    db.create_all()
    click.echo('Initialized database.')

在这个命令函数前,我们使用 click 提供的 option 装饰器为命令添加了一个 --drop 选项,将 is_flag 参数设为 True 可以将这个选项声明为布尔值标志 (boolean flag)。--drop 选项的值作为 drop 参数传入命令函数,如果提供了 这个选项,那么 drop 的值将是 True ,否则为 False。因为添加 --drop 选项会直接清空数据库内容,如果需要,也可以通过 click.confirm() 函数添加一个 确认提示,这样只有输入 y 或 yes 才会继续执行操作。

现在,执行下面的命令会重建数据库和表:

shell
flask init --drop

当使用 SQLite 时,直接删除 data.db 文件和调用 drop_all() 方法效果相同,而且更直接,不容易出错。