AI 应用后端工程化:从原型到可交付系统 · 第 02 篇 · 第一章 · 服务基础
数据库接入真正困难的是结构如何演进、失败如何回滚,以及开发环境和生产环境如何保持一致。
数据库第一次接入通常很顺利:定义模型、创建表、插入一行数据。真正的麻烦从第二次修改开始。字段要改名、旧数据不能丢、线上还有请求正在读旧结构。这时“ORM 能自动建表”远远不够,我们需要同时理解模型、事务和迁移扮演的角色。
三张不同的地图
ORM 模型像建筑的当前平面图,迁移文件像每次施工记录,事务则像一次施工期间的安全围栏。三者都与数据库有关,却不能互相替代。
flowchart TD
Model[应用模型\n当前期望结构] --> Diff[结构差异]
History[迁移历史\n已发生的变化] --> Diff
Diff --> Migration[下一次迁移]
Migration --> Transaction[事务执行]
Transaction -->|成功| Schema[新数据库结构]
Transaction -->|失败| Rollback[回滚到一致状态]
只保留模型而没有迁移历史,相当于只知道房子今天长什么样,却不知道旧住户怎样安全搬到新布局。只写迁移而不更新模型,应用仍会按照旧地图访问。没有事务,一次变更执行到一半失败,数据库便可能同时存在两套世界。
先区分连接生命周期和事务生命周期
连接负责与数据库通信,事务负责界定一组操作是否共同成功。它们经常被一个 session 对象包装,容易让人误以为是同一件事。
def rename_workspace(session, workspace_id: int, new_name: str) -> None:
workspace = session.get(Workspace, workspace_id)
if workspace is None:
raise LookupError("workspace not found")
workspace.name = new_name.strip()
if not workspace.name:
raise ValueError("name must not be empty")
session.commit()
在真实服务里,commit 更适合由用例边界统一管理:用例内部可能同时更新工作区、写审计记录、创建通知。如果每个仓储函数各自提交,第三步失败时,前两步已经无法一起回滚。
更稳妥的规则是“一次业务动作对应一个事务”。仓储负责读写实体,用例决定何时提交,框架中间件负责异常时回滚和最终释放连接。这条规则同样适用于订单创建、配额扣减和文档入库。
迁移是可执行的历史
开发者常见的危险动作是直接修改线上表,再把 ORM 模型改到一致。短期看很快,之后新环境却无法重现这次手工操作。迁移文件的价值正在于:任何人从空数据库开始,都能按顺序得到同一个结构;任何环境都能回答“当前执行到了哪一版”。
一个字段改名也不只是 ALTER TABLE。如果服务需要不停机,可以分为四步:
- 先新增新字段,应用同时兼容新旧字段。
- 后台回填历史数据,并监控未回填数量。
- 切换读取和写入到新字段,观察一个发布周期。
- 最后删除旧字段和兼容代码。
这种“扩展再收缩”比一次破坏性修改慢,却允许新旧版本在滚动发布期间短暂共存。迁移工具只负责执行步骤,不会替团队判断兼容窗口。
自动生成不等于自动正确
迁移工具可以比较模型与数据库,生成候选脚本,但它看不懂业务语义。把 full_name 改成 display_name,工具可能判断为删除旧列再新增列,历史姓名就会全部消失。把可空字段改成非空,它也不知道应该为旧数据填什么。
因此迁移评审至少检查四件事:是否丢数据、是否长时间锁表、旧版本应用能否继续运行、失败后能否回退。生产执行前应在接近真实规模的数据副本上测量耗时,而不是只在十行测试数据上看到“执行成功”。
失败现场比成功演示更有价值
设想创建知识条目时先插入主记录,再插入片段列表。第十五个片段违反唯一约束,如果主记录已经提前提交,系统会留下一个显示“创建成功”却没有完整内容的空壳。下次重试可能又因为主记录存在而失败。
事务可以保证原子性,但仍需考虑外部副作用。数据库事务无法撤销已经上传到对象存储的文件,也无法收回已经发送的短信。此时可以先提交数据库中的“待处理”状态,再由幂等任务执行外部动作;或者使用 outbox 表把要发布的事件与业务数据放进同一事务。事务边界是设计问题,不只是 commit() 的位置。
从 AI 后端迁移到普通系统
模型配置、文档片段和对话记录只是数据类型,背后的方法与电商订单完全一致。需要版本历史的配置可以采用不可变版本表,需要软删除的数据应明确唯一约束范围,需要高频查询的关系要在真实访问模式确定后再加索引。
练习可以从一张 projects(id, name) 表开始:设计一次不停机迁移,为它增加非空的 slug 字段。写出新增、回填、切换、收缩四个阶段,并为每个阶段注明旧应用是否还能工作。只要其中一个阶段答案含糊,就说明迁移方案还不能进入生产。
两次提交带来的提醒
这段演进只有两步,却揭示了数据库接入的分水岭:能读写数据只是起点,能让结构在多个环境中安全变化,才算真正进入工程化。