创建时间: 2026-09-01最后更新: 2026-09-01

1. 数据与持久化

到目前为止,我们写过的 FastAPI 接口大多在函数里临时构造结果。请求到来时,Python 创建字典或对象;请求结束后,如果没有把这些数据保存到外部介质,它们就只能留在当前进程的内存中。服务一旦重启,数据也会随之消失。

真实项目不能只依赖进程内存。用户注册之后,下次访问时账号仍然应该存在;一条任务被标记为完成,服务重启后也不能恢复成原来的状态。让数据跨越请求和进程继续存在,就是持久化。

最简单的持久化方式是写文件:

save_to_file.py
1
import json
2
3
tasks = [
4
{"id": 1, "title": "学习 PostgreSQL", "status": "todo"},
5
]
6
7
with open("tasks.json", "w", encoding="utf-8") as file:
8
json.dump(tasks, file, ensure_ascii=False)

对于一次性脚本或很小的本地工具,这种方式完全可用。但当文件成为 Web 应用的主要数据来源后,问题很快就会出现:

  • 两个请求同时修改文件时,怎样避免后一次写入覆盖前一次结果;
  • 数据越来越多之后,怎样快速查找某个用户的任务;
  • 如何保证每条任务引用的项目确实存在;
  • 修改多份数据时,怎样让它们要么一起成功,要么一起失败;
  • 服务部署多个实例后,怎样共享数据并看到一致的结果;
  • 进程或机器意外中断后,怎样恢复到可以继续使用的状态

因此,数据库就成为了更好的选择。

数据库是把数据组织、查询、约束、事务、并发控制和故障恢复放进一套经过长期验证的解决方案中,让应用不必从零实现这些复杂能力

2. 数据库系统

PostgreSQL、MySQL 和 SQLite 都属于常见的数据库管理系统,但它们的运行方式并不完全相同。

PostgreSQL 和 MySQL 通常作为独立服务运行,应用通过连接与它们通信;SQLite 则常作为库嵌入应用进程,直接读写数据库文件。

我们的任务管理系统包含清晰的用户、项目和任务关系,还需要唯一性、外键和事务,因此关系型数据库很合适。后续课程会以 PostgreSQL 为具体实现,学习这些通用概念。

3. 关系模型

关系型数据库从逻辑上使用表组织数据。一张任务表可以表示为:

idproject_idtitlestatus
110学习 PostgreSQLtodo
210完成接口测试done

表中的几个基础概念分别是:

  • 表描述同一类事物,例如 tasks;
  • 列描述这类事物具有哪些属性,例如 title 列和 status 列;
  • 行表示一条具体记录,例如编号为 1 的任务是学习 PostgreSQL;
  • 值是某行某列中的内容,例如 todo;
  • 数据类型限定一列可以保存和处理哪些值;
  • 数据库结构也常写作 database schema,用来描述表、列、类型和约束等定义。

这里的 database schema 表示数据库的整体逻辑结构。PostgreSQL 还把数据库内部的命名空间称为 Schema,例如默认的 public。两个概念使用了同一个词,但所指的范围不同,下一篇会结合 PostgreSQL 的层级继续区分

这里还有两个容易忽略的规则。

第一,表中的行没有默认顺序。即使某次查询恰好按照 id 返回,也不能依赖这个现象;只有显式使用 ORDER BY,结果顺序才有保证。

ordered-tasks.sql
1
SELECT id, title, status
2
FROM tasks
3
ORDER BY id;

第二,SQL 本身不会自动给每行分配业务主键。如果建表时没有声明主键或唯一约束,数据库可以接受内容完全相同的多行。实际业务表通常都需要一个稳定的主键来识别记录。

4. 键、关系与完整性

一张表可以保存一类记录,但真实数据通常不是彼此孤立的。关系型数据库使用键把不同表连接起来。我们可以把上一节的 tasks 表与它依赖的 users、projects 表放在一起观察。

users 表保存用户自身的信息:

idemaildisplay_name
1xiaoming@example.com小明
2xiaohong@example.com小红

projects 表保存项目。owner_id 不再重复记录用户名和邮箱,只记录所属用户的 id:

idowner_idname
101数据库学习
202FastAPI 实践

tasks 表采用同样的方式,通过 project_id 记录任务属于哪个项目:

idproject_idtitlestatus
110学习 PostgreSQLtodo
210完成接口测试done
320设计路由doing

现在顺着表中的值来看:projects 表中编号为 10 的项目,其 owner_id 是 1,因此它属于 users 表中编号为 1 的小明;tasks 表中前两行的 project_id 都是 10,因此这两条任务属于同一个“数据库学习”项目。

这两层引用关系可以写成:

relationships.txt
1
projects.owner_id -> users.id
2
tasks.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

request-boundary.txt
1
FastAPI 进程
2
-> Python 数据库驱动
3
-> 数据库连接
4
-> PostgreSQL 服务
5
-> PostgreSQL 管理的数据文件

6. SQL

SQL 是应用与关系型数据库沟通的主要语言。它更接近描述想要什么结果,例如,下面的语句描述了我们想读取项目 10 中尚未完成的任务,并按编号排序:

find-tasks.sql
1
SELECT id, title
2
FROM tasks
3
WHERE project_id = 10
4
AND status <> 'done'
5
ORDER 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 一行代码更长:

query-flow.txt
1
浏览器请求
2
-> FastAPI 路由与业务逻辑
3
-> 数据库驱动获取连接并发送 SQL
4
-> PostgreSQL 解析并检查 SQL
5
-> 查询规划器选择执行计划
6
-> 执行器读取、过滤或关联数据
7
-> 驱动把结果转换成 Python 值
8
-> FastAPI 序列化 HTTP 响应

数据库收到 SQL 后,首先需要分析语法与对象引用,然后由规划器比较可选路径。例如,一条查询可能顺序扫描整张表,也可能使用索引;涉及多张表时,还要选择连接顺序和连接算法。执行器按照最终计划取得数据,并把结果返回客户端。

读取数据也不等于每次都直接读取磁盘。PostgreSQL 和操作系统都会利用内存缓存,执行器只负责按照计划访问存储系统。我们写业务 SQL 时通常不控制这些细节,但分析性能时需要知道它们存在。

一次请求也不应该每执行一条 SQL 就重新建立物理连接。Web 应用通常使用连接池复用有限数量的数据库连接。连接池能降低连接成本,却不能让数据库拥有无限并发能力;池大小、请求时长和事务边界仍然需要共同设计

8. 事务与并发

数据库的另一项核心能力,是让多条相关操作保持一致。假设完成任务时既要修改当前状态,又要写入状态历史:

complete-task.txt
1
更新 tasks.status
2
写入 task_status_history

如果第一步成功、第二步失败,系统就会出现状态已经改变但没有历史记录的不完整结果。事务可以把两步组成一个工作单元:全部成功时提交,任意一步失败时回滚。

事务通常用 ACID 概括:

性质含义
原子性 Atomicity一组操作要么全部生效,要么都不生效
一致性 Consistency提交后的数据继续满足已经定义的规则
隔离性 Isolation并发事务不会随意看到彼此未完成的中间状态
持久性 Durability提交成功的数据在故障恢复后仍应保留

ACID 不是数据库自动理解业务的承诺。一致性依赖我们正确设计约束与事务边界,隔离性还受隔离级别和具体读写方式影响。即使两段代码都放进事务,也不代表所有并发竞态会自动消失。

PostgreSQL 使用多版本并发控制,也就是 MVCC,让语句根据事务隔离规则读取一致的数据快照,并配合锁处理写入冲突。这使多个请求可以同时访问数据库,但应用仍然需要识别重复提交、丢失更新和死锁等情况。后续事务与并发章节会逐步展开这些问题。

正在验证登录状态
请稍候,验证完成后将继续显示文章内容