1. 数据与持久化
到目前为止,我们写过的 FastAPI 接口大多在函数里临时构造结果。请求到来时,Python 创建字典或对象;请求结束后,如果没有把这些数据保存到外部介质,它们就只能留在当前进程的内存中。服务一旦重启,数据也会随之消失。
真实项目不能只依赖进程内存。用户注册之后,下次访问时账号仍然应该存在;一条任务被标记为完成,服务重启后也不能恢复成原来的状态。让数据跨越请求和进程继续存在,就是持久化。
最简单的持久化方式是写文件:
1import json23tasks = [4{"id": 1, "title": "学习 PostgreSQL", "status": "todo"},5]67with open("tasks.json", "w", encoding="utf-8") as file:8json.dump(tasks, file, ensure_ascii=False)
对于一次性脚本或很小的本地工具,这种方式完全可用。但当文件成为 Web 应用的主要数据来源后,问题很快就会出现:
- 两个请求同时修改文件时,怎样避免后一次写入覆盖前一次结果;
- 数据越来越多之后,怎样快速查找某个用户的任务;
- 如何保证每条任务引用的项目确实存在;
- 修改多份数据时,怎样让它们要么一起成功,要么一起失败;
- 服务部署多个实例后,怎样共享数据并看到一致的结果;
- 进程或机器意外中断后,怎样恢复到可以继续使用的状态
因此,数据库就成为了更好的选择。
数据库是把数据组织、查询、约束、事务、并发控制和故障恢复放进一套经过长期验证的解决方案中,让应用不必从零实现这些复杂能力
2. 数据库系统
PostgreSQL、MySQL 和 SQLite 都属于常见的数据库管理系统,但它们的运行方式并不完全相同。
PostgreSQL 和 MySQL 通常作为独立服务运行,应用通过连接与它们通信;SQLite 则常作为库嵌入应用进程,直接读写数据库文件。
我们的任务管理系统包含清晰的用户、项目和任务关系,还需要唯一性、外键和事务,因此关系型数据库很合适。后续课程会以 PostgreSQL 为具体实现,学习这些通用概念。
3. 关系模型
关系型数据库从逻辑上使用表组织数据。一张任务表可以表示为:
| id | project_id | title | status |
|---|---|---|---|
| 1 | 10 | 学习 PostgreSQL | todo |
| 2 | 10 | 完成接口测试 | done |
表中的几个基础概念分别是:
- 表描述同一类事物,例如
tasks; - 列描述这类事物具有哪些属性,例如
title列和status列; - 行表示一条具体记录,例如编号为 1 的任务是学习 PostgreSQL;
- 值是某行某列中的内容,例如
todo; - 数据类型限定一列可以保存和处理哪些值;
- 数据库结构也常写作 database schema,用来描述表、列、类型和约束等定义。
这里的 database schema 表示数据库的整体逻辑结构。PostgreSQL 还把数据库内部的命名空间称为 Schema,例如默认的 public。两个概念使用了同一个词,但所指的范围不同,下一篇会结合 PostgreSQL 的层级继续区分
这里还有两个容易忽略的规则。
第一,表中的行没有默认顺序。即使某次查询恰好按照 id 返回,也不能依赖这个现象;只有显式使用 ORDER BY,结果顺序才有保证。
1SELECT id, title, status2FROM tasks3ORDER BY id;
第二,SQL 本身不会自动给每行分配业务主键。如果建表时没有声明主键或唯一约束,数据库可以接受内容完全相同的多行。实际业务表通常都需要一个稳定的主键来识别记录。
4. 键、关系与完整性
一张表可以保存一类记录,但真实数据通常不是彼此孤立的。关系型数据库使用键把不同表连接起来。我们可以把上一节的 tasks 表与它依赖的 users、projects 表放在一起观察。
users 表保存用户自身的信息:
| id | display_name | |
|---|---|---|
| 1 | xiaoming@example.com | 小明 |
| 2 | xiaohong@example.com | 小红 |
projects 表保存项目。owner_id 不再重复记录用户名和邮箱,只记录所属用户的 id:
| id | owner_id | name |
|---|---|---|
| 10 | 1 | 数据库学习 |
| 20 | 2 | FastAPI 实践 |
tasks 表采用同样的方式,通过 project_id 记录任务属于哪个项目:
| id | project_id | title | status |
|---|---|---|---|
| 1 | 10 | 学习 PostgreSQL | todo |
| 2 | 10 | 完成接口测试 | done |
| 3 | 20 | 设计路由 | doing |
现在顺着表中的值来看:projects 表中编号为 10 的项目,其 owner_id 是 1,因此它属于 users 表中编号为 1 的小明;tasks 表中前两行的 project_id 都是 10,因此这两条任务属于同一个“数据库学习”项目。
这两层引用关系可以写成:
1projects.owner_id -> users.id2tasks.project_id -> projects.id
projects.owner_id 引用 users.id,tasks.project_id 引用 projects.id。这样既能表达表之间的关系,又能避免同一事实被复制到多张表中。围绕这些字段,数据库通常会定义三种规则:
- 主键唯一标识一行,例如
users.id; - 外键要求引用值能在目标表中找到,例如
tasks.project_id引用projects.id; - 唯一约束保证某个值或一组值不重复,例如用户邮箱。
外键负责维护参照完整性,但它不会自动表示所有业务要求。例如外键列如果允许 NULL,子表记录仍然可以暂时没有对应的父表记录;如果这列必须有引用,还要同时添加 NOT NULL。
数据库中的完整性大致可以从三个层面理解:
| 完整性 | 解决的问题 | 常见手段 |
|---|---|---|
| 取值完整性 | 字段能保存什么值 | 数据类型、NOT NULL、CHECK |
| 实体完整性 | 怎样唯一识别一行 | PRIMARY KEY |
| 参照完整性 | 表之间的引用是否有效 | FOREIGN KEY |
5. PostgreSQL
我们选择 PostgreSQL 作为主要数据库。它是关系型数据库管理系统,采用客户端与服务端架构,适合从本地开发一直使用到生产环境。
选择 PostgreSQL,并不是因为所有项目都只能使用它,而是它能把接下来要学习的能力连成一套完整体系:
- 以 SQL 为核心完成数据定义、写入和查询;
- 使用丰富的类型和约束保护数据;
- 使用事务处理一组相关操作;
- 通过多版本并发控制和锁协调并发读写;
- 使用索引、统计信息和执行计划分析查询性能;
- 使用 JSONB、UUID 等能力处理现代 Web 项目的常见数据;
- 通过成熟的 Python 驱动和 SQLAlchemy 接入 FastAPI。
PostgreSQL 是独立运行的服务。FastAPI 不会直接操作 PostgreSQL 的数据文件,而是通过数据库驱动建立连接,再向服务端发送 SQL
1FastAPI 进程2-> Python 数据库驱动3-> 数据库连接4-> PostgreSQL 服务5-> PostgreSQL 管理的数据文件
6. SQL
SQL 是应用与关系型数据库沟通的主要语言。它更接近描述想要什么结果,例如,下面的语句描述了我们想读取项目 10 中尚未完成的任务,并按编号排序:
1SELECT id, title2FROM tasks3WHERE project_id = 104AND status <> 'done'5ORDER BY id;
至于是扫描整张表,还是通过索引定位数据,由 PostgreSQL 的查询规划器结合表结构、索引和统计信息决定。SQL 相同,数据规模不同,数据库选择的执行方式也可能不同。
学习时通常会按用途对 SQL 语句分类。下面的分类便于建立概念,但不是唯一的分类标准:
| 用途 | 常见语句 | 作用 |
|---|---|---|
| 结构定义 | CREATE、ALTER、DROP | 创建或修改数据库结构 |
| 数据查询 | SELECT | 读取、过滤和组合数据 |
| 数据变更 | INSERT、UPDATE、DELETE、MERGE | 新增、修改或删除行 |
| 事务控制 | BEGIN、COMMIT、ROLLBACK | 控制一组操作的完整性 |
| 权限控制 | GRANT、REVOKE | 限制角色可以执行的操作 |
业务开发中常说的 CRUD,可以对应到 INSERT、SELECT、UPDATE 和 DELETE。但真正掌握数据库不能停留在四条语句上,还要理解关系、连接、约束、事务和执行计划。
后续文章会先直接学习 SQL,再进入 Python 驱动和 ORM。ORM 能帮助我们组织数据访问代码,但它最终仍然要生成 SQL。不了解数据库实际执行的语句,遇到慢查询、事务错误或关系加载问题时就很难定位原因。
7. 一条查询如何执行
当浏览器请求任务列表时,真正的调用过程比 SELECT 一行代码更长:
1浏览器请求2-> FastAPI 路由与业务逻辑3-> 数据库驱动获取连接并发送 SQL4-> PostgreSQL 解析并检查 SQL5-> 查询规划器选择执行计划6-> 执行器读取、过滤或关联数据7-> 驱动把结果转换成 Python 值8-> FastAPI 序列化 HTTP 响应
数据库收到 SQL 后,首先需要分析语法与对象引用,然后由规划器比较可选路径。例如,一条查询可能顺序扫描整张表,也可能使用索引;涉及多张表时,还要选择连接顺序和连接算法。执行器按照最终计划取得数据,并把结果返回客户端。
读取数据也不等于每次都直接读取磁盘。PostgreSQL 和操作系统都会利用内存缓存,执行器只负责按照计划访问存储系统。我们写业务 SQL 时通常不控制这些细节,但分析性能时需要知道它们存在。
一次请求也不应该每执行一条 SQL 就重新建立物理连接。Web 应用通常使用连接池复用有限数量的数据库连接。连接池能降低连接成本,却不能让数据库拥有无限并发能力;池大小、请求时长和事务边界仍然需要共同设计
8. 事务与并发
数据库的另一项核心能力,是让多条相关操作保持一致。假设完成任务时既要修改当前状态,又要写入状态历史:
1更新 tasks.status2写入 task_status_history
如果第一步成功、第二步失败,系统就会出现状态已经改变但没有历史记录的不完整结果。事务可以把两步组成一个工作单元:全部成功时提交,任意一步失败时回滚。
事务通常用 ACID 概括:
| 性质 | 含义 |
|---|---|
| 原子性 Atomicity | 一组操作要么全部生效,要么都不生效 |
| 一致性 Consistency | 提交后的数据继续满足已经定义的规则 |
| 隔离性 Isolation | 并发事务不会随意看到彼此未完成的中间状态 |
| 持久性 Durability | 提交成功的数据在故障恢复后仍应保留 |
ACID 不是数据库自动理解业务的承诺。一致性依赖我们正确设计约束与事务边界,隔离性还受隔离级别和具体读写方式影响。即使两段代码都放进事务,也不代表所有并发竞态会自动消失。
PostgreSQL 使用多版本并发控制,也就是 MVCC,让语句根据事务隔离规则读取一致的数据快照,并配合锁处理写入冲突。这使多个请求可以同时访问数据库,但应用仍然需要识别重复提交、丢失更新和死锁等情况。后续事务与并发章节会逐步展开这些问题。