表单草稿机制

Odoo onchange 为什么不能替代业务逻辑:草稿记录、RecordSnapshot 与最终 write 边界讲透

从 Odoo 19 的 web onchange 实现出发,解释 onchange 为什么本质上是在表单草稿上做推演,而不是在数据库里真正写记录;也讲清它和 create/write/constraints 该如何分工。

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

先打掉一个最常见误会

很多人第一次用 Odoo 的 @api.onchange,会不自觉把它理解成:

  • 字段变了
  • 服务器立刻真的改数据库
  • 所以后端业务逻辑可以放在 onchange 里

这其实是错位的。

onchange 更准确的定位是:

在表单编辑过程中,对“草稿态记录”做一次服务器辅助推演,然后把差异回传给前端。

它解决的是交互体验问题,不是数据库事实落定问题。


第一层:new() 已经暴露了 onchange 的本质

odoo/orm/models.py 里,new() 的注释非常关键:

返回一个附着在当前环境上的新记录实例,这条记录并不存在于数据库,只存在于内存中。

而 web 模块的 onchange() 正是基于这种记录在工作。

也就是说,onchange 处理的往往不是“真实已保存记录”,而是:

  • 当前表单上的值
  • 默认值
  • 用户刚刚改过但还没保存的值
  • 以及若干 x2many 草稿行

所以从第一性原理上,它就不适合承担“最终业务事实”的职责。


第二层:web onchange 真正在做什么

/home/ubuntu/odoo-temp/addons/web/models/models.py 里的 onchange() 流程非常值得看。

它大致会做这些事:

  1. 把前端传来的当前表单 values 收进来
  2. 如果是首次调用,补默认值
  3. 预取 x2many 相关数据,减少不必要计算
  4. self.new(...) 构造一份内存草稿记录
  5. 构建 RecordSnapshot 记录初始快照
  6. 应用 changed values
  7. 调用 _apply_onchange_methods() 执行对应 onchange 方法
  8. 再做一份最终快照
  9. 用两个快照的 diff,把变化回给前端

注意,这整个过程的关键词都不是:

  • commit
  • insert
  • update 数据库

而是:

  • draft
  • snapshot
  • diff
  • return value

这已经说明它的核心职责就是表单推演


第三层:为什么 RecordSnapshot 很关键

RecordSnapshot 这个类很能说明问题。

它会:

  • 抓一份当前记录在指定字段树下的值
  • 对 x2many 子行继续递归做 snapshot
  • 之后比较 before / after
  • 最终只把“差异”回传

所以 onchange 并不是“服务器替你保存了一遍”,而是:

服务器帮你算了一遍草稿应该长什么样,然后把变化增量告诉前端。

这就是为什么:

  • 你在表单上看到字段跟着跳了
  • 但数据库里其实还没变

很多新手第一次对这个边界没概念,就会写出“界面上看着对,保存后却不对”的代码。


第四层:为什么 onchange 不能替代 create/write

因为它们解决的问题根本不同。

onchange

解决:

  • 用户改字段时,界面应该怎么即时响应
  • 默认值、联动值、warning 怎么更顺手
  • 草稿态 x2many 行怎么跟着变

create / write

解决:

  • 最终要落什么数据库事实
  • 所有入口都必须遵守哪些业务规则
  • RPC、import、cron、server action、脚本调用都要不要一致

如果你把真正业务约束写在 onchange 里,会发生什么?

  • 表单里操作看起来没问题
  • 但脚本 create() 没走这个逻辑
  • import 没走
  • 自动任务没走
  • API 调用也没走

于是系统行为开始分裂。

这就是典型的“表单里对,系统里不对”。


第五层:为什么它也不能替代 constraints

同理,constraints 的职责是兜底校验:

  • 只要数据要落库
  • 不管从哪条入口来
  • 规则都必须成立

而 onchange 最多只能做到:

  • 早点提醒
  • 帮用户少填错
  • 在前端交互阶段把明显不合理的组合改一改

所以最佳实践通常是:

  • onchange 做体验引导
  • create/write 做业务落地
  • constraints 做最终兜底

这三个层次不要互相替代。


一个很典型的误区

例如你在销售订单行里写:

  • 选了产品后自动带出价格、税、描述

这个非常适合 onchange。

但如果你写的是:

  • 不允许某类客户下某类产品
  • 某种组合一旦出现必须自动拆单
  • 某些状态转换必须记录审计字段

这些就不该只放 onchange。

因为它们不是“界面联动”,而是业务事实


为什么 x2many 场景更容易让人误判

web onchange 里有不少代码专门处理:

  • one2many / many2many 预取
  • NewId
  • command diff
  • line snapshot 对比

这说明 Odoo 为了让表单联动自然,已经做了很多草稿态模拟。

但也正因为模拟得太像真的,开发者更容易误会:

  • “子行都改了,肯定已经写库了吧?”

并没有。

很多时候那只是:

  • 前端拿到一串 command
  • 页面把草稿重新渲染出来
  • 等你点保存时,才真正进 create / write

一个最稳的分工原则

以后写功能时,可以这样判断:

适合放 onchange 的

  • 自动带值
  • 动态域 / 提示
  • 价格、描述、税、联系人等表单联动
  • warning 提示

不该只放 onchange 的

  • 安全校验
  • 状态流转硬约束
  • 任何必须对 import / API / cron 同样生效的逻辑
  • 审计与最终落库行为

如果一条规则离开表单后也必须成立,就别只写 onchange。


一句话总结

Odoo 的 onchange 本质不是“提前写库”,而是“基于 new() 草稿记录和 RecordSnapshot 差异计算,为表单返回一份更合理的草稿状态”。它适合做交互联动,不适合单独承担 create/write/constraints 那种最终业务责任。

DISCUSSION

评论区

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