速读#
这篇把接口从返回死数据推进到真正落库,重点不是 CRUD 写法,而是第一次接数据库时的几个判断。SQLite 适合练手,是因为零运维阻力;SQL 占位符不是优化,而是从第一天就该守住的安全底线。
练手库:按运维阻力选,不按并发能力选#
| 类型 | 代表 | 代价 |
|---|---|---|
| 轻量内嵌 | SQLite | 零服务、零端口;并发写弱、文件锁 |
| 独立服务 | MySQL / Postgres | 要起服务、配账号、管连接池;扛并发 |
不是好差之分,是现阶段最怕哪种麻烦。练手我怕装环境的阻力,不怕并发;真上线扛流量时,怕的就反过来了。
我选 SQLite:练手要零运维阻力,不是并发能力。 短板清楚,这个阶段不构成问题;哪天真要扛并发,再换独立服务的数据库不迟。
Pydantic:把数据形状写进代码#
class NoteIn(BaseModel): title: str body: str
class Note(NoteIn): id: int用 dict 传来传去,字段对不对全靠人肉记。Pydantic 让「一条笔记长什么样」成为看得见、能校验的契约——和 FastAPI「接口定义即文档」同一思路的延伸。
NoteIn / Note 拆两层:创建时不带 id,输出带库分配的 id。分开建模,比一个模型塞所有字段清晰。
占位符是底线,不是优化#
cur = conn.execute( "INSERT INTO notes(title, body) VALUES (?, ?)", (note.title, note.body),)? 占位符,不要字符串拼接。这不是「最佳实践」这种客套话,是防 SQL 注入的底线。 占位符做的事很本质:把「语句」和「数据」分开走,用户输入永远以数据身份进入,写什么都成不了语句的一部分;拼接则相反,数据随时可能被解释成语句。从第一天就别图省事——拼接一旦成了习惯,哪天一处忘了处理,就是一个洞。
坑:init_db() 放错地方#
一开始把 init_db() 写在路由里,每次请求建一次表。有 IF NOT EXISTS 不报错,但每次连库纯属浪费。放到模块加载时执行一次就够。
能跑 ≠ 对。 不报错让我以为没问题,浪费在悄悄发生。这类问题比报错更阴:没有任何信号提醒你去看它。
间歇 500#
接口每隔二三十次请求 500 一次,日志里是 database is locked。最后定位到并发下的 SQLite 文件锁:它的写锁是库级的,同一时刻只允许一个写入者;后来的写入者会先等一小段(Python 的 sqlite3 默认给 5 秒),等不到才抛这个错。
当时想不通的是:我明明没开任何线程,并发从哪来?从框架来——FastAPI 会把同步写法的路由函数放进线程池执行,几个请求同时进来,就是几个线程在同时写库。所以单线程的本地测试永远复现不了,一接真实请求就间歇性出现。
这次留下的排查心智:稳定复现的 bug 好查,间歇性的 bug 先怀疑并发和竞态。 出现概率跟请求频率走的,基本是共享资源在打架。
选了零运维的方便,就要接受并发短板;要扛并发,换库或加队列,别在 SQLite 上硬扛。(SQLite 的 WAL 模式能让读和写并行,但写入者依然只有一个——那是缓解,不是解除。)
三个判断带走:
- 练手库按阻力选
- 数据形状显式建模
- 占位符是底线不是优化
