计算字段重算队列

store=True 的计算字段为什么有时不立刻更新:Odoo 重算队列到底怎么跑?

很多人以为 compute 方法一跑,store=True 字段就会立刻写回数据库。但从 Odoo ORM 源码看,真实机制是“先标记待重算,再在合适时机批量 recompute”。理解 tocompute、precompute、flush 和 _recompute_field,才能真正看懂为什么某些值看起来会“晚一步”。

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

很多开发者第一次接触 Odoo 计算字段时,都会形成一个很自然的直觉:

我改了依赖字段,compute 不就应该立刻跑,store=True 的值也应该马上更新吗?

这个直觉不算错,但只对了一半

/home/ubuntu/odoo-temp/odoo/orm/models.pyodoo/orm/fields.py 里,Odoo 的真实做法不是“每改一次就同步重算一次”,而是:

  1. 先标记哪些字段需要重算;
  2. 把这些字段放进事务里的 tocompute 集合;
  3. 到合适时机再批量 _recompute_field()
  4. 由字段对象的 recompute()compute_value() 真正把值算出来并写回缓存/数据库。

这就是为什么你有时会看到下面这些现象:

  • 表单里值已经变了,但数据库还没立刻更新;
  • 一批记录一起创建时,计算字段不像你想的那样“一条条算”;
  • store=True 的字段表现得像“晚一步”;
  • 有的字段创建前就算好了,有的却要等 flush 之后才出现正确结果。

如果不理解这条链路,就很容易把正常行为误判成 bug。

一、store=True 不等于“同步即时写库”

先抓住一个最关键的概念:

store=True 解决的是“结果是否持久化”,不是“何时立刻执行”。

源码里真正控制时机的,不只是 store=True,还包括:

  • 字段有没有 precompute=True
  • 当前记录是不是新纪录
  • 事务里有没有把该字段加入 tocompute
  • 什么时候触发 flush / create / write 后续流程
  • 字段是否递归、是否 compute_sudo

也就是说,store=True 只是告诉 ORM:

这个字段最终要落库。

但它不承诺每次依赖变化都马上单条重算

二、Odoo 的核心思路:先“登记待算”,再“批量执行”

models.py_recompute_recordset()_recompute_field() 非常能说明问题。

逻辑大意是:

  • 找出当前模型或当前记录集里,哪些字段既 computestore
  • 去事务的 tocompute 里拿待重算的 id
  • 如果当前这批记录里确实有这些 id,就调 field.recompute(records)

也就是说,Odoo 不是靠“依赖一变化就立刻直接写库”来保证一致性,而是靠:

  • 依赖追踪
  • 待重算登记
  • 批量执行

来保证最终一致性。

这样设计的好处很现实:

1)避免重复计算

如果你在一次 write() 里改了多个依赖字段,ORM 没必要每改一个字段就重算一次。

2)更适合批量场景

一批记录一起创建或更新时,统一重算比逐条重算更容易利用 prefetch 和缓存。

3)让依赖图更可控

复杂模型里,字段彼此依赖很多。如果全都同步立即计算,性能和递归控制都会更难。

三、为什么有些字段“创建前就有值”,有些要晚一点?

答案就在 precompute=True

fields.py 的字段参数说明写得很直白:

  • precompute=True 表示字段可以在插入数据库前先算好;
  • 但如果这个字段本身有显式值或默认值,预计算可能不会发生;
  • 预计算并不总是更快,单条创建时甚至可能更慢;
  • 对 one2many 行这类通常批量生成的数据,precompute=True 反而常常很合适。

models.py 里的 _add_precomputed_values() 做的事,就是先找出所有 precompute=True 的字段,再对缺失值的新记录跑一次“创建前补值”。

这就解释了一个常见现象:

情况 A:字段是 precompute=True

你在 create() 之后几乎立刻就能看到值,因为它在真正 insert 前已经被补进待写数据里了。

情况 B:字段只是 store=True,但不是 precompute=True

它仍然会持久化,但通常更偏向在后续 flush / recompute 阶段统一处理。

所以:

不是所有 stored compute 都在同一个时间点完成。

四、fields.recompute() 真正做了什么

fields.py 里的 recompute() 很值得读。

它先从事务里拿当前字段对应的 to_compute_ids,如果没有就直接返回。也就是说:

没被登记进待重算队列的记录,字段不会无缘无故自己跑。

然后它会分两种情况:

1)递归字段

如果字段声明了 recursive=True,源码会按记录逐条处理,避免递归依赖把整批数据绕乱。

2)普通字段

普通字段会尽量按批次处理,调用 compute_value();如果中途碰到访问权限问题,再退回单条处理。

这个细节很重要,因为它说明 Odoo 的重算策略不是一把梭:

  • 能批量就尽量批量;
  • 批量不稳时再缩小粒度;
  • 缺记录、权限差异、递归依赖都会影响执行方式。

五、compute_value() 不只是“调用你的方法”

很多人以为 compute 很简单:就是执行你写的 _compute_xxx()

但在框架层,compute_value() 还处理了两个很关键的边界:

1)compute_sudo

如果字段配置了 compute_sudo,框架会先切到 sudo() 再算。

这意味着:

  • 同一个 compute 方法,普通用户和超级用户看到的依赖记录范围可能不同;
  • 有些“为什么这个字段在后台是对的,前台却不对”的问题,其实和 compute_sudo 有关。

2)即使 compute 方法没正确赋值,框架也要维护计算状态

Odoo 要尽量保证“哪些记录还待算、哪些已经算过”这件事不会乱掉,否则很容易陷入无限重算或脏缓存。

所以 compute 不是单纯的 Python 回调,它是 ORM 事务一致性的一部分。

六、为什么你会感觉“值晚一步”?

实战里最常见的错觉有三种。

1)把缓存变化当成数据库变化

表单上或当前 recordset 上看到的新值,很多时候先反映的是缓存,不一定代表数据库已经在那个瞬间完成落库。

2)把 onchange 和 compute 混为一谈

onchange 是前端交互期的“临时响应”,主要服务表单体验; compute 是 ORM 语义的一部分,重点是依赖一致性和最终结果。

两者都会让你“看到值变化”,但触发时机和可靠性完全不同。

3)忽略批量优化

Odoo 很多地方宁可晚一点统一做,也不愿意每次写操作都立即把整条依赖链逐个炸一遍。

从框架角度看,这通常是正确选择。

七、开发时最容易踩的坑

坑 1:以为 store=True 就一定“实时”

不对。

store=True 保障的是结果可搜索、可分组、可持久化,不等于 UI 级别的“立刻可见”。

坑 2:需要创建前值却没用 precompute=True

如果字段参与:

  • 必填校验
  • 约束判断
  • 插入时就依赖该值的下游逻辑

那就要认真考虑是否适合 precompute=True

坑 3:compute 里写了搜索或重逻辑,却期望它频繁即时运行

这样很容易把一次普通 write() 变成性能灾难。

坑 4:忘了权限边界

某些 compute 依赖的记录普通用户看不到,结果会导致:

  • 值和管理员看到的不一样;
  • 批量计算时偶发 AccessError;
  • 调试时“本地能复现、线上又不稳定”。

八、什么时候该用 precompute=True

可以用一个很实用的判断法:

适合 precompute=True 的情况

  • 字段能在 insert 前确定;
  • 不依赖数据库里已存在的聚合统计;
  • 经常作为 one2many 行批量创建的一部分;
  • 创建后立刻就要用于 required / constraints / 业务判断。

不一定适合的情况

  • 依赖复杂搜索、聚合或跨大量记录统计;
  • 记录往往一条条创建,批量收益不明显;
  • 计算很重,反而更适合后续统一批量处理。

九、排查 stored compute 问题的顺序

如果你遇到“值不对”或“更新晚一步”,我建议按这个顺序排查:

  1. 先看字段是不是 store=True,是不是还声明了 precompute=True
  2. 确认依赖是不是都写在 @api.depends
  3. 区分你看到的是缓存值、onchange 值,还是数据库持久化后的值
  4. 看 create/write 是单条还是批量
  5. 检查 compute 是否依赖权限受限数据,是否需要 compute_sudo
  6. 最后再怀疑 ORM 本身。

多数时候,问题不是 Odoo “没算”,而是你期待的时机和框架实际选择的时机不一样。

总结

Odoo 对 store=True 计算字段的真实策略,不是“依赖一变就立即写库”,而是:

  • 先登记待重算
  • 按事务和批量机会统一执行
  • 必要时在创建前通过 precompute=True 提前补值
  • 再由 recompute() / compute_value() 把结果安全落地。

理解这套机制之后,你会更容易判断:

  • 哪些“晚一步”是正常现象;
  • 哪些场景该用 precompute=True
  • 哪些 compute 写法会拖垮性能;
  • 以及为什么 stored compute 的问题,本质上常常不是“字段没更新”,而是你还没站在 ORM 的时间线上看它。

DISCUSSION

评论区

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