速读#
这篇把接口从返回死数据推进到真正落库,重点不是 CRUD 写法,而是第一次接数据库时的几个判断。SQLite 适合练手,是因为零运维阻力;SQL 占位符不是优化,而是从第一天就该守住的安全底线。
练手库:按运维阻力选,不按并发能力选#
| 类型 | 代表 | 代价 |
|---|---|---|
| 轻量内嵌 | SQLite | 无独立服务和端口;仍要管文件、备份、迁移与并发写 |
| 独立服务 | MySQL / Postgres | 要起服务、配账号、管连接池;扛并发 |
不是好差之分,是现阶段最怕哪种麻烦。练手我怕装环境的阻力,不怕并发;真上线扛流量时,怕的就反过来了。
我选 SQLite,是为了把独立数据库服务的运维阻力拿掉,不是因为它只能练手。 低写入并发、单机部署的真实应用也能长期使用 SQLite;反过来,一旦需要多台应用实例共享数据库、持续高并发写或成熟权限治理,就应尽早评估 PostgreSQL、MySQL 等服务型数据库。迁移不只是换连接串,还涉及 SQL 方言、数据搬迁、主键、事务与上线切换,不能把它当成零成本后手。
Pydantic:把数据形状写进代码#
from pydantic import BaseModel, Field
class NoteCreate(BaseModel): title: str = Field(min_length=1, max_length=200) body: str = Field(min_length=1)
class NoteOut(NoteCreate): id: int用 dict 传来传去,字段对不对全靠人肉记。Pydantic 让“一条笔记长什么样”成为看得见、能校验的 HTTP 契约,长度限制也会进入 OpenAPI。它只守应用入口,不能替数据库约束:表里仍要有 NOT NULL、合适的长度或 CHECK、唯一性与外键,因为脚本、迁移和其他写入路径可能绕开 FastAPI。
NoteCreate / NoteOut 拆两层:创建时不接受客户端指定 id,输出才带数据库分配的 id。更新接口通常还会有字段可选的 NoteUpdate,但没有更新需求时不提前造第三个模型。
占位符是底线,不是优化#
cur = conn.execute( "INSERT INTO notes(title, body) VALUES (?, ?)", (note.title, note.body),)? 占位符,不要字符串拼接。这不是“优化”,而是防 SQL 注入的底线。 驱动把 SQL 结构和参数值分开处理,标题里即使含引号也只会成为数据。占位符只能代替值,不能代替表名、列名或 ASC / DESC;这些动态结构必须从服务端白名单映射,不能把用户输入直接拼进去。
坑:把初始化当成请求逻辑或导入副作用#
一开始把 init_db() 写在路由里,每次请求都建表;后来又差点把它挪到模块顶层,变成导入即执行。前者重复做无用工作,后者在测试、CLI 和多 worker 启动时都会触发,执行次数也不是“整个服务一次”。小应用可以放进 FastAPI lifespan,让初始化属于应用生命周期:
from contextlib import asynccontextmanagerimport sqlite3from fastapi import FastAPI
DB_PATH = "notes.db"
@asynccontextmanagerasync def lifespan(app: FastAPI): with sqlite3.connect(DB_PATH) as conn: conn.execute(""" CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY, title TEXT NOT NULL CHECK(length(title) BETWEEN 1 AND 200), body TEXT NOT NULL CHECK(length(body) > 0) ) """) yield
app = FastAPI(lifespan=lifespan)多 worker 仍会各跑一次 lifespan,因此 DDL 必须幂等;正式 schema 变更则应在发布前由迁移步骤执行,而不是让每个应用进程抢着改表。
能跑 ≠ 对。 不报错让我以为没问题,浪费在悄悄发生。这类问题比报错更阴:没有任何信号提醒你去看它。
间歇 500#
接口每隔二三十次请求 500 一次,日志里是 database is locked。SQLite 同一时刻只能有一个写事务;后来的写入会等待连接配置的 timeout,Python sqlite3.connect() 默认约 5 秒,仍拿不到锁才抛错。真正容易放大问题的是事务开得太早、提交太晚,尤其在事务里夹着网络请求或慢计算。
当时想不通的是:我明明没手动开线程,并发从哪来?FastAPI 会把普通 def 路由放到线程池执行,多个请求可以同时进入同步数据库代码。每次操作应创建自己的 SQLite 连接,并用上下文管理器在成功时提交、异常时回滚;不要跨线程共享默认连接,也不要把连接长期挂在全局变量上。
这次留下的排查心智:稳定复现的 bug 好查,间歇性的 bug 先怀疑并发和竞态。 出现概率跟请求频率走的,基本是共享资源在打架。
修复顺序是先缩短事务、设置合理的等待时间并让客户端有限重试,再根据场景启用 WAL 改善读写并行;WAL 仍然只有一个写者,也不适合把数据库文件随意放到不可靠的网络文件系统。若业务天然需要持续并发写、多实例共享或长事务,就换服务型数据库;队列只能把写入串行化并增加延迟与故障处理,不是无条件更简单的答案。
三个判断带走:
- 练手库按阻力选
- 数据形状显式建模
- 占位符是底线不是优化
