Skip to content

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 能自动处理:

  • 参数校验;
  • 默认值;
  • 类型转换;
  • 接口文档生成。

例如 pagepage_size 可以设置默认值和说明。

分页查询默认不查总数

很多后台系统分页时都会显示总条数。

但查总数并不总是便宜。

当数据量很大、查询条件复杂时,count 会给数据库带来额外压力。

所以课程里采用一种性能更友好的方式:

  • 默认不查总数;
  • 每次多查一条;
  • 如果多出来一条,说明还有下一页;
  • 用户需要总数时,再单独查询。

这能减少每次分页请求对数据库的压力。

为什么多查一条

假设每页显示 10 条。

后端实际查 11 条。

如果查到了 11 条,说明还有下一页;返回给前端时只返回前 10 条。

如果只查到 10 条或更少,说明没有下一页。

这样不需要 count(*),也能告诉前端还能不能继续翻页。

新建数据

新建数据的流程比较直接。

大致步骤:

  1. 接收前端传来的数据;
  2. 转成 SQLModel 对象;
  3. 加入 session;
  4. 提交事务;
  5. 返回新建后的数据。

提交成功后,可以回到数据库工具里刷新表,确认数据已经插入。

如果字段没存进去,先检查前端传参字段名和后端 model 字段名是否一致。

例如驼峰命名和下划线命名没有处理好,就可能出现某些字段为空。

更新数据要先查询

更新数据不能直接把前端传来的 model 对象丢进 session。

原因是前端传来的对象只是普通数据对象,它没有和数据库 session 建立连接。

正确思路是:

  1. 先根据 ID 从数据库查出已有记录;
  2. 把前端传来的字段更新到这个已连接对象上;
  3. 把查出来的对象加入 session;
  4. 提交事务。

如果不先查询,直接 add 一个带主键的对象,ORM 可能会当作插入处理,导致主键重复错误。

exclude_unset 的意义

更新时通常不想把前端没传的字段也覆盖掉。

可以把前端传来的 model 转成字典,并只取真正传入的字段。

这样可以实现“局部更新”。

例如只修改名称,就不要把其他字段都写成空值。

这一节的重点

这一节不是只写几个接口,而是在建立后端数据库开发的基本套路:

  • 配置数据库连接;
  • 建表和导入数据;
  • 定义 SQLModel;
  • 用 Router 管理接口;
  • 用 Pydantic 描述参数;
  • 实现分页查询;
  • 实现新建和更新;
  • 注意性能和 ORM 对象状态。

后面做 AI 简历和通用业务表格时,都会复用这套数据库操作思路。

AI Agent 课程学习文档。