切换日光/暗黑模式
028. SQLModel 增删改查与分页查询
学习目标
这一节开始写后端数据库操作,使用 SQLModel 连接 MySQL,并实现基础增删改查。
学完后,你应该能理解:
- 同步阻塞为什么会影响后端接口;
- 多 worker 和线程池分别缓解什么问题;
- SQLModel 和 SQLAlchemy、Pydantic 的关系;
- 后端为什么要配置 MySQL 连接信息;
- 为什么建表通常不直接交给程序自动生成;
- 如何用 FastAPI Router 管理接口前缀和分组;
- 分页查询为什么默认不查总数;
- 更新数据为什么要先查出来再修改。
同步阻塞和多 worker
如果某个接口内部执行了同步阻塞代码,后面的请求可能会被它挡住。
前面已经用线程池处理过这类问题。线程池属于线程级别的调度。
另一种方式是启动多个 worker。
多 worker 属于进程级别的调度。一个请求阻塞时,其他 worker 仍然可以处理别的请求。
但这不是免费能力:
- worker 越多,占用资源越多;
- 小服务器不适合开太多 worker;
- 入口参数要使用 import string;
- 启动日志可能会打印多次,因为应用文件会被多个进程加载。
如果服务器只有 2 核 4G,一开始开一个实例就够了。不要为了“看起来更强”盲目加 worker。
worker 启动参数
使用 Uvicorn 启动多个 worker 时,不能直接把 app 对象传进去。
需要传入类似字符串形式的应用入口。
原因是多个进程需要根据这个字符串重新导入应用模块。
如果传错,会出现类似必须使用 import string 才能启用 reload 或 workers 的错误。
这类错误不是业务代码问题,而是启动方式不符合 Uvicorn 的要求。
SQLModel 的位置
后端数据库操作使用 SQLModel。
它可以理解成结合了:
- SQLAlchemy 的数据库 ORM 能力;
- Pydantic 的数据校验和类型声明能力;
- Python 类型提示。
SQLModel 不是一个只写 SQL 的工具,它更强调用 Python 类来描述表结构和数据对象。
对于 JS 开发者,可以把它粗略类比成“带类型和校验能力的 ORM”。
安装依赖
需要安装 SQLModel 以及相关依赖。
课程里还补充了 greenlet 这类数据库相关依赖。
依赖安装后,会写入项目依赖配置文件。后续别人安装项目依赖时,也能得到相同包。
安装依赖不是只让当前机器能跑,还要让项目配置记录下来。
配置 MySQL 连接信息
后端要连接 MySQL,需要在环境变量里配置连接信息。
常见字段包括:
- 主机地址;
- 端口;
- 用户名;
- 密码;
- 数据库名;
- 字符集或其他连接参数。
这些配置通常放在 .env.example 和真实 .env 里。
.env.example 告诉别人需要哪些字段;真实 .env 保存你自己的服务器信息。
MySQL 是后端业务数据的核心配置,所以可以放在配置文件靠前的位置,方便查看和修改。
手动建表
这一节使用 SQL 语句手动创建测试表,例如 llm_demo。
虽然 ORM 有自动建表能力,但真实项目里经常会手动管理表结构或通过迁移脚本管理。
原因是自动建表风险较高:
- 线上表结构不能随便自动改;
- 数据库变更需要可审查;
- 多环境要保持一致;
- 表结构变化可能影响已有数据。
学习阶段先用 SQL 建表,可以更清楚地看到数据库里到底创建了什么。
导入测试数据
建表后,再执行插入语句导入测试数据。
用数据库工具打开表,可以确认数据是否已经写入。
数据库工具还能根据 SQL 别名提示字段。
例如给表起别名后,通过 t1. 可以提示这个表有哪些字段。这能减少手写字段名时的错误。
找回建表语句
如果忘了某张表的建表 SQL,可以在数据库工具里生成 DDL。
这会把当前表结构转换成建表语句。
这个能力很适合:
- 记录表结构;
- 排查字段类型;
- 复制到文档;
- 给同事同步表定义;
- 对比不同环境表结构。
定义 Model 类
每张表需要一个对应的 Python model。
例如表名是 llm_demo,可以定义 LLMDemoModel。
它需要继承 SQLModel,并声明:
- 表名;
- 主键;
- 字段;
- 字段类型;
- 是否可选;
- 默认值。
model 类是后端代码和数据库表之间的桥梁。
数据库里是表和字段;Python 里是类和属性。
使用 Router 分组接口
FastAPI 可以用 APIRouter 给一组接口加前缀和分组。
例如把 LLM demo 相关接口放到同一个 router 里。
这样接口文档里会更清晰,也方便后续维护。
如果所有接口都直接挂在 app 上,项目变大后会很乱。
查询参数用 Pydantic 描述
分页查询需要参数,例如:
- 当前页;
- 每页数量;
- 查询条件;
- 排序条件。
这些参数可以用 Pydantic 的 BaseModel 描述。
这样 FastAPI 能自动处理:
- 参数校验;
- 默认值;
- 类型转换;
- 接口文档生成。
例如 page 和 page_size 可以设置默认值和说明。
分页查询默认不查总数
很多后台系统分页时都会显示总条数。
但查总数并不总是便宜。
当数据量很大、查询条件复杂时,count 会给数据库带来额外压力。
所以课程里采用一种性能更友好的方式:
- 默认不查总数;
- 每次多查一条;
- 如果多出来一条,说明还有下一页;
- 用户需要总数时,再单独查询。
这能减少每次分页请求对数据库的压力。
为什么多查一条
假设每页显示 10 条。
后端实际查 11 条。
如果查到了 11 条,说明还有下一页;返回给前端时只返回前 10 条。
如果只查到 10 条或更少,说明没有下一页。
这样不需要 count(*),也能告诉前端还能不能继续翻页。
新建数据
新建数据的流程比较直接。
大致步骤:
- 接收前端传来的数据;
- 转成 SQLModel 对象;
- 加入 session;
- 提交事务;
- 返回新建后的数据。
提交成功后,可以回到数据库工具里刷新表,确认数据已经插入。
如果字段没存进去,先检查前端传参字段名和后端 model 字段名是否一致。
例如驼峰命名和下划线命名没有处理好,就可能出现某些字段为空。
更新数据要先查询
更新数据不能直接把前端传来的 model 对象丢进 session。
原因是前端传来的对象只是普通数据对象,它没有和数据库 session 建立连接。
正确思路是:
- 先根据 ID 从数据库查出已有记录;
- 把前端传来的字段更新到这个已连接对象上;
- 把查出来的对象加入 session;
- 提交事务。
如果不先查询,直接 add 一个带主键的对象,ORM 可能会当作插入处理,导致主键重复错误。
exclude_unset 的意义
更新时通常不想把前端没传的字段也覆盖掉。
可以把前端传来的 model 转成字典,并只取真正传入的字段。
这样可以实现“局部更新”。
例如只修改名称,就不要把其他字段都写成空值。
这一节的重点
这一节不是只写几个接口,而是在建立后端数据库开发的基本套路:
- 配置数据库连接;
- 建表和导入数据;
- 定义 SQLModel;
- 用 Router 管理接口;
- 用 Pydantic 描述参数;
- 实现分页查询;
- 实现新建和更新;
- 注意性能和 ORM 对象状态。
后面做 AI 简历和通用业务表格时,都会复用这套数据库操作思路。