很多开发者第一次接触 Odoo 计算字段时,都会形成一个很自然的直觉:
我改了依赖字段,
compute不就应该立刻跑,store=True的值也应该马上更新吗?
这个直觉不算错,但只对了一半。
在 /home/ubuntu/odoo-temp/odoo/orm/models.py 和 odoo/orm/fields.py 里,Odoo 的真实做法不是“每改一次就同步重算一次”,而是:
- 先标记哪些字段需要重算;
- 把这些字段放进事务里的
tocompute集合; - 到合适时机再批量
_recompute_field(); - 由字段对象的
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() 非常能说明问题。
逻辑大意是:
- 找出当前模型或当前记录集里,哪些字段既
compute又store - 去事务的
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 问题的顺序
如果你遇到“值不对”或“更新晚一步”,我建议按这个顺序排查:
- 先看字段是不是
store=True,是不是还声明了precompute=True; - 确认依赖是不是都写在
@api.depends里; - 区分你看到的是缓存值、onchange 值,还是数据库持久化后的值;
- 看 create/write 是单条还是批量;
- 检查 compute 是否依赖权限受限数据,是否需要
compute_sudo; - 最后再怀疑 ORM 本身。
多数时候,问题不是 Odoo “没算”,而是你期待的时机和框架实际选择的时机不一样。
总结
Odoo 对 store=True 计算字段的真实策略,不是“依赖一变就立即写库”,而是:
- 先登记待重算;
- 按事务和批量机会统一执行;
- 必要时在创建前通过
precompute=True提前补值; - 再由
recompute()/compute_value()把结果安全落地。
理解这套机制之后,你会更容易判断:
- 哪些“晚一步”是正常现象;
- 哪些场景该用
precompute=True; - 哪些 compute 写法会拖垮性能;
- 以及为什么 stored compute 的问题,本质上常常不是“字段没更新”,而是你还没站在 ORM 的时间线上看它。
DISCUSSION
评论区