很多人对“保存”这件事的想象过于朴素
在不少系统里,前端点保存,开发者脑内自动翻译成:
- 已有记录 →
write(vals) - 新记录 →
create(vals) - 返回成功 → 结束
但 Odoo Web 客户端的常见链路并不只是“写一下数据库”。
在 addons/web/models/models.py 里,web_save() 的实现非常直白,却也非常说明问题:
- 如果当前 recordset 有记录 →
write(vals) - 否则 →
create(vals) - 如果带了
next_id→ 再browse(next_id) - 最后统一走
with_context(bin_size=True).web_read(specification)回读
所以你前端点保存时,真正发生的是:
先持久化,再按前端要求的 specification 回读一遍结果。
这和“只做 write”是两个完全不同的心智模型。
为什么保存后还要再读一遍
因为前端要的不是“数据库里成功了”这句空话。
前端真正需要的是:
- 保存后的最终字段值
- many2one 的显示名
- x2many 需要展示哪些子字段
- 二进制字段是否只回 bin size
- 某些 NewId 临时值如何转回真实 id
也就是说,保存动作的返回值,本质上是:
一份为当前界面量身定制的最新记录快照。
这就是 web_save() 最后必须接 web_read(specification) 的原因。
web_save() 为什么比你想的更“前端协议化”
看实现会发现,它不是一个纯 ORM 风格的底层方法,而更像:
- Web Client 和 ORM 之间的协议适配层
因为它关心的不是“怎么把 vals 落库就完”,而是“落库后如何把结果重新包装成前端马上能继续渲染的结构”。
这点在 web_read() 里表现得特别明显。
比如:
- many2one 可能不只是回 id,还会回嵌套字段
- 如果 specification 里要求
display_name,系统会专门补展示名 - x2many 可以带
fields、order、limit、context - 新建未落库记录的
NewId需要清理成前端能理解的 id/origin 形态
所以很多“为什么我保存后页面上看到的值和单纯 read 结果不一样”的现象,都不是 bug,而是因为:
你看到的是
web_read语义下、按 specification 组装过的结果。
新建和编辑为什么会共用同一条保存入口
web_save() 里一个很优雅的点是:
- 有记录 →
write - 没记录 →
create
这让前端不必为“当前是新建还是编辑”维护两套完全不同的持久化协议。
对 Web Client 来说,保存更像是一件抽象动作:
- 我把当前 form buffer 交给服务端
- 服务端决定是 create 还是 write
- 然后给我一份最新可渲染结果
这种统一接口也是为什么 Odoo 前端在 form 场景里能比较自然地处理:
- 新建记录
- 已存在记录
- 带子表的编辑
- 保存后继续停留当前页
next_id 为什么值得注意
源码里有个很多人会忽略的小细节:
if next_id:
self = self.browse(next_id)
这说明保存动作的“回读目标”不一定等于刚刚写入的那个 recordset。
它可能出现在这样的场景里:
- 前端希望在保存后切到另一条记录
- 某些流程保存后跳到新生成或下一条记录
- UI 导航状态和 ORM 当前对象不完全等价
所以如果你调试“为什么保存后页面展示的是另一条数据”,不能只盯着 create/write 本身,还得看是否传了 next_id。
为什么保存后看到的 many2one / x2many 结果常常和你预期不同
因为 web_read() 不是朴素的 read() 包装。
many2one
如果 specification 没要求嵌套字段,很多场景只会处理 id 语义;
如果要求了 display_name 或其他字段,它会去 co-record 再读,并按前端结构回填。
x2many
它还能吃:
fieldsorderlimitcontext
也就是说,保存返回的子表内容可能是:
- 排过序的
- 截断过的
- 带特定上下文的
- 不是数据库自然顺序
所以别把保存返回体误当成“数据库最原始状态快照”。
它是面向当前页面的渲染快照。
这对后端开发有什么直接影响
1. 不要只测 create() / write(),还要测保存后的显示结果
很多后端逻辑本身没问题,但用户说“保存后页面不对”,真正出问题的其实是:
- 相关字段回读没带出来
display_name语义和你想的不一样- x2many specification 导致返回结果排序或裁剪了
2. 设计字段时要考虑 Web 回读成本
如果表单保存后一定会立即回读一大串 related/x2many 字段,那你的字段设计、计算成本和依赖链都会直接影响交互手感。
3. 调试前后端问题时,把“保存”和“回显”拆开看
很多问题不是“写没成功”,而是:
- 写成功了
- 但回读规格让前端看到另一种结构
这两步混在一起看,很容易误判。
为什么 web_save 这个名字很诚实
它不是 ORM 的通用“保存”方法,而是 Web 语义下的保存:
- 以 Web Client 的 specification 为中心
- 以回显所需数据结构为中心
- 把 create/write/read 三件事连成一个前端友好的动作
所以它真正表达的是:
不是只保存数据库,而是完成一次 Web 表单状态的持久化与回显闭环。
一句话记住
如果只记一句,就记这句:
在 Odoo Web 表单里,“保存”通常不是只调用一次 create/write,而是 create/write 之后立刻按 specification 再做一次 web_read。
这就是为什么很多表单问题,真正该查的是:
- 持久化逻辑
- 回读 specification
- 保存后目标记录是谁
三者一起看,才能看清完整链路。
DISCUSSION
评论区