跳到正文
CRUD:我第一次让接口和数据库对上号的那几个判断
CRUD:我第一次让接口和数据库对上号的那几个判断

CRUD:我第一次让接口和数据库对上号的那几个判断

光返回死数据不算后端。这篇给接口接上 SQLite。不讲 CRUD 怎么写,讲我为什么选 SQLite 练手、Pydantic 模型替我管了什么,以及「占位符不是图省事」从第一天就该刻进去。

速读#

这篇把接口从返回死数据推进到真正落库,重点不是 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 模式能让读和写并行,但写入者依然只有一个——那是缓解,不是解除。)

三个判断带走:

  1. 练手库按阻力选
  2. 数据形状显式建模
  3. 占位符是底线不是优化
版权许可

CC BY-NC-SA 4.0 本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

相关文章

s1oopX

登录 s1oopX