先打掉一个最常见误会
很多人第一次用 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() 流程非常值得看。
它大致会做这些事:
- 把前端传来的当前表单 values 收进来
- 如果是首次调用,补默认值
- 预取 x2many 相关数据,减少不必要计算
- 用
self.new(...)构造一份内存草稿记录 - 构建
RecordSnapshot记录初始快照 - 应用 changed values
- 调用
_apply_onchange_methods()执行对应 onchange 方法 - 再做一份最终快照
- 用两个快照的 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
评论区