模型写入管线

Odoo 表单里改了值,保存时到底发生了什么:onchange、compute、constraints 与 write 管线讲透

很多开发者会把 onchange、compute、constraints 都当成‘字段变了就会触发的东西’,结果一到线上就困惑:为什么界面上是对的,保存后又变了?为什么 onchange 跑了,但约束还没报?本文把表单编辑到真正写库这条管线按源码拆开。

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

很多 Odoo 初学者会把这几样东西混成一个概念:

  • @api.onchange
  • compute
  • @api.constrains
  • write() / create()

因为从表面上看,它们都像是:

字段一变,系统就会“做点什么”。

但如果你真的按这个模糊印象写模块,很容易遇到这些经典困惑:

  • 页面上看着是对的,保存后却变了
  • onchange 明明跑了,为什么约束没报
  • compute 明明会算,为什么数据库里还没更新
  • 某些逻辑创建时生效,编辑时却不生效

根本原因是:

这几套机制处在同一条表单管线上,但分别负责不同阶段。

一、先分清:onchange 是“表单态逻辑”,不是最终持久化逻辑

addons/web/models/models.py 里,onchange() 的实现写得很清楚。

它接收:

  • 当前表单 values
  • 本次改动的 field_names
  • 视图里的 fields_spec

然后会构造一个 new record / snapshot 语境,在缓存里应用改动,再返回要回填给前端的 value / warning。

这意味着什么?

1)onchange 的主战场是内存态 / 表单态

它解决的是:

  • 用户刚改了字段
  • 界面现在应该联动出什么结果
  • 要不要立刻给 warning

2)onchange 不等于已经写进数据库

它更像一个“试算 / 联动层”。

所以你在表单里看到的结果,很多时候只是:

当前交互上下文里,Odoo 认为界面该怎么表现。

如果后面没有真正 create()write(),这些结果不一定成为数据库里的最终事实。

二、compute 负责的是字段依赖结果,不等于每次都在同一时刻落库

很多人看到 compute 字段,会误以为它和 onchange 类似,都是“字段一改就立刻执行完毕”。

但 ORM 里的 modified()_recompute_recordset() 已经说明了真实情况:

  • 依赖字段变化后,系统会标记哪些 compute 字段受影响
  • 某些字段会进入待重算集合
  • 之后在合适时机 recompute / flush

所以 compute 更像:

数据依赖层

它的目标不是“让用户看起来联动”,而是“保证字段依赖关系最终一致”。

这也是为什么:

  • 有些 compute 在交互中你能立刻看到
  • 有些是保存后才稳定
  • 有些 stored compute 会先经历缓存失效、重算、再写库

三、@api.constrains 不是联动器,而是持久化前后的业务守门员

odoo/orm/models.py_validate_fields() 里,Odoo 会根据本次涉及的字段,去触发对应约束方法。

这非常关键。

约束的关注点不是:

  • 页面联动得顺不顺
  • 用户输入过程中体验好不好

而是:

当前这次创建 / 写入,最终允许不允许成为业务事实。

所以你会看到一个很典型的现象:

  • 表单上 onchange 早就跑过了
  • 但只有点击保存后,@api.constrains 才真正挡你

这不是延迟,而是职责不同。

四、write() 才是“真正写库管线”的核心入口

odoo/orm/models.py 里的 write(),你会发现它不是单纯的 SQL update 包装。

它会依次做很多事:

  1. 检查访问权限和字段权限
  2. 整理要写入的 field/value
  3. 保护 inverse / editable compute 字段
  4. 对关系字段先 modified(..., before=True)
  5. 实际写各字段
  6. modified(vals),让依赖重算进入正确队列
  7. _validate_fields(...) 跑约束
  8. 必要时执行 inverse、recompute、flush 等后续动作

也就是说:

真正的保存不是“一次 update”,而是一条 ORM 管线。

这条管线里,权限、缓存、重算、约束、inverse 都是串起来的。

五、为什么“界面上对,保存后又变了”特别常见

因为你看到的是两层世界:

第一层:交互世界

onchange 主导。

它强调的是:

  • 立刻联动
  • 给用户反馈
  • 临时修正表单值

第二层:持久化世界

write() / create()computeconstraints 主导。

它强调的是:

  • 最终数据是否允许入库
  • 依赖字段最后怎么一致化
  • 约束是否允许通过

如果你把第一层误当成第二层,就会觉得“系统反复横跳”。

实际上它只是:

  • 先在表单态帮你演算
  • 再在持久化态严格落规则

六、create()write() 相似,但不是完全相同

create() 也会:

  • 检查字段权限
  • 处理 defaults
  • 建记录缓存
  • 标记 compute 字段待重算
  • 对 stored 字段跑 _validate_fields()

但它处理的是“从没有到有”的过程。

所以很多逻辑会在 create 阶段天然带上:

  • default_get
  • context 默认值
  • 新记录缓存初始化

而 write 处理的是“已有记录再修改”。

因此同一个模块如果你只测编辑、不测新建,或者只测新建、不测编辑,很容易漏掉问题。

七、开发里最容易踩的四个坑

1)把 onchange 当成后端业务规则

这是最常见的大坑。

如果你的逻辑必须在导入、RPC、自动化任务、后端 create/write 时都成立,那它不能只放在 onchange

因为 onchange 更多是表单交互入口,不是所有写入入口都会走它。

2)把 constraints 当成前端联动提示

约束是守门,不是引导。

它适合阻止非法数据入库,不适合承担“用户一输入就友好提示”的全部职责。

3)以为 compute 一定比约束早或一定比约束晚

真实顺序取决于字段类型、是否 stored、是否进入 recompute、何时 flush。

不要靠猜顺序写代码,要基于 ORM 语义写。

4)在 write 里自己手搓太多二次同步

Odoo 本身已经有 modified()、recompute、inverse、constraints 这套机制。

如果你在 write() 里再手工到处补写,很容易造成:

  • 顺序混乱
  • 重复计算
  • 性能问题
  • 难以追踪的副作用

八、一个实用心智模型

如果你想把整条链记得很清楚,可以这样理解:

用户改字段时

先走 表单联动层onchange

用户点击保存时

进入 ORM 写入层create() / write()

写入过程中

系统维护 依赖一致性层modified() / _recompute_recordset() / compute

写入落地前后

系统执行 业务守门层_validate_fields() / @api.constrains

这样一分层,你就不会再拿错工具。

九、实战建议:每种逻辑放在最合适的层

  • 界面联动、默认填充、即时 warningonchange
  • 最终一致的数据派生 → compute
  • 禁止非法业务状态入库 → constraints
  • 真正的数据持久化与流程编排 → create / write

如果一个规则同时需要“用户体验好”和“后端永远成立”,通常要两层都做:

  • 前端层用 onchange 提示或预填
  • 后端层用 constraints / create / write 保底

最后总结

Odoo 表单保存时的真实世界,不是“字段变了就触发一个魔法函数”,而是一条分层管线:

  • onchange 负责交互态
  • compute 负责依赖一致性
  • constraints 负责业务守门
  • create/write 负责真正写库

所以以后再遇到“页面对、保存后不对”这类问题,别再把四种机制混成一个词了。

先问自己:

这个逻辑,应该属于交互层、依赖层、守门层,还是持久化层?

你把层放对了,Odoo 开发会顺很多。

DISCUSSION

评论区

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