计算字段

Odoo 存储计算字段为什么看起来“没立刻更新”:depends、modified 与重算队列讲透

很多人以为 stored compute 字段会像普通 Python 赋值一样立即改完、立即稳定。但从 fields.py 的 compute_value 和 models.py 的 modified、_recompute_recordset 看,Odoo 真正维护的是一条“依赖标记 -> 加入待重算 -> 适时 flush/recompute”的管线。

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

很多开发者第一次遇到 stored compute 字段,心里想的是一种很直觉的模型:

  • 依赖字段改了;
  • compute 方法马上跑;
  • 新值立刻落稳;
  • 后面所有逻辑都应该看到最终值。

但 Odoo 真实实现并不是“字段一变就同步立刻算完”。

/home/ubuntu/odoo-temp/odoo/orm/fields.pyodoo/orm/models.py 看,Odoo 更像在维护一条调度链路:

  1. 哪些字段被改了;
  2. 谁依赖这些字段;
  3. 哪些记录要加入待重算集合;
  4. 什么时候真正 recompute;
  5. 什么时候 flush 回数据库。

所以很多“为什么它看起来没立刻更新”的现象,根源不是 compute 失效,而是你把 Odoo 想成了同步赋值脚本。

一、depends 不是“立刻执行”,而是“建立触发关系”

源码里 modified() 的注释写得很直白:

  • 它要根据字段依赖树,找出哪些 stored compute 字段需要重算;
  • 然后把这些字段和记录标记到待计算集合里。

也就是说,依赖声明的核心作用不是“此刻立刻计算”,而是:

当这些字段变化时,系统知道后面该重算谁。

这也是为什么你写了 @api.depends(...) 之后,看到的是“加入重算管线”,而不一定是“在你当前这行代码下一秒就完成所有最终副作用”。

二、modified() 干的是“标记与传播”

models.py 里的 modified() 很值得仔细看。

它会:

  • 沿着依赖树往后找;
  • 把受影响的 stored compute 字段加入 tocompute
  • 对非 stored compute 更多是做缓存失效,而不是强制写库。

这意味着两件事:

1)Odoo 在区分“要不要写库”

  • stored compute:需要后续重算并可能落库;
  • non-stored compute:更多是失效缓存,等真正访问时再算。

2)Odoo 在区分“现在算”还是“稍后算”

字段变化之后,并不要求每次都当场把整棵依赖树完全算穿。

这正是 ORM 规模上还能跑得动的重要原因。

三、真正计算时,compute_value() 还会先处理待重算集合

fields.py 里的 compute_value() 也透露出一个很重要的信号:

  • 对 stored compute 字段,会先把对应记录从待重算集合里移除;
  • 再在保护上下文里执行 _compute_field_value()
  • 如果失败,再把它重新放回待重算集合。

这说明 Odoo 不是“算一下就完”,而是在维护一种相对稳的事务语义:

  • 先声明“这批我要开始算了”;
  • 算成功,任务完成;
  • 算失败,任务重新挂回去。

这也是为什么复杂 compute 出错时,你常会看到后续链路表现得像“还有东西没算干净”。

四、_recompute_recordset()flush_recordset() 才是很多人忽略的时机点

_recompute_recordset() 会针对记录集,把待重算的 stored compute 字段真正处理掉。

flush_recordset() 又会先触发 _recompute_recordset(),再把数据刷向数据库。

因此实战里你经常会遇到这种错觉:

  • Python 里刚改完字段;
  • 马上看数据库,值还没变;
  • 或者同一事务里某些地方看到新值,某些地方还像旧值。

这通常不是 Odoo 混乱,而是:

你正好站在“已标记待重算,但尚未完成 flush/recompute”的中间状态上。

五、为什么 stored compute 最怕副作用思维

很多人写 compute 时,会顺手干这些事:

  • 写别的字段;
  • 建消息;
  • 改状态;
  • 触发外部动作。

这很危险。

因为 compute 本来是依赖驱动的结果计算,它的执行时机可能来自:

  • create;
  • write;
  • flush;
  • 其他字段连锁重算;
  • 访问字段时的懒计算场景。

如果你把它当成“稳定的一次性业务钩子”,就很容易出现:

  • 重复副作用;
  • 顺序错判;
  • 性能爆炸;
  • 调试时完全说不清到底是谁先触发谁。

六、新手最常见的三个误解

1)“stored = 立刻写库”

不对。stored 的意思是“这个值最终落库”,不是“每次字段变化都同步即时写库完成”。

2)“depends = 一改就马上跑到最终态”

不对。depends 首先是依赖声明,后面还有标记、重算、flush 这些阶段。

3)“我现在看到旧值,就是 compute 没触发”

也不一定。它可能已经进入 tocompute,只是还没在你观察的时机点完成重算。

七、实战里该怎么设计 compute 才更稳

我更建议遵守这几条:

  1. compute 尽量纯函数化:根据依赖算结果,少做副作用;
  2. 把硬校验放约束或业务方法,不要塞进 compute;
  3. 把通知、建单、写日志这类动作 放在明确业务事件里;
  4. 性能敏感时先想依赖粒度,不要让一个小变动牵动太大范围重算;
  5. 调试时别只盯数据库值,还要意识到 ORM 里存在“待重算中间态”。

总结

Odoo 的 stored compute 字段,本质上不是“同步赋值”,而是“依赖驱动的重算管线”。

从源码看,关键环节是:

  • depends 定义触发关系;
  • modified() 负责传播与标记;
  • compute_value() 真正执行计算并维护待算集合;
  • _recompute_recordset() / flush_recordset() 决定什么时候把结果算实、刷实。

如果只记一句,就记这句:

stored compute 的核心不是“立刻”,而是“最终一致地被重算并落库”。

DISCUSSION

评论区

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