表单保存链路

Odoo 表单点保存时到底是不是直接 write:web_save、create/write 与回读链路讲透

很多人把前端保存理解成“RPC 调一次 write 就结束”。但 web 客户端真实常用的是 web_save + web_read 组合。本文从 web/models/models.py 源码讲清 create/write、next_id 与保存后回读为什么会影响你看到的结果。

Odoo 开发 前端
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 9 阅读

很多人对“保存”这件事的想象过于朴素

在不少系统里,前端点保存,开发者脑内自动翻译成:

  • 已有记录 → 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 可以带 fieldsorderlimitcontext
  • 新建未落库记录的 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

它还能吃:

  • fields
  • order
  • limit
  • context

也就是说,保存返回的子表内容可能是:

  • 排过序的
  • 截断过的
  • 带特定上下文的
  • 不是数据库自然顺序

所以别把保存返回体误当成“数据库最原始状态快照”。

它是面向当前页面的渲染快照


这对后端开发有什么直接影响

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

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。