在 Odoo 里,很多人第一次看到 precompute=True,会很自然地把它理解成:
这个字段会“更早一点”被 compute,一般应该更快。
这个理解只对了一半。
从 /home/ubuntu/odoo-temp/odoo/orm/fields.py 和 odoo/orm/models.py 的源码,以及 addons/test_orm/tests/test_fields.py 里的测试来看,precompute 不是一个泛用性能开关,而是一个严格限定在 create 前阶段的机制。它有明确收益,也有非常容易被忽略的代价。
一、precompute 真正解决的,不是“更快”,而是“插入前必须有值”
在 fields.py 的字段参数说明里,Odoo 对 precompute 写得很直白:
- 它表示字段会在记录插入数据库前被计算
- 适用于那些在插入前就可以安全算出的值
- 常见动机包括:避免插入后再额外查询、让 required/constraints 更早拿到值
这意味着它首先是一个时机控制机制,而不是一个抽象的“性能优化标签”。
尤其在 models.py 的 _add_precomputed_values() 里,框架会:
- 找出模型上所有
precompute=True的字段 - 对缺失这些字段值的
vals构造new()记录 - 直接访问这些字段,让 compute 在插入前得到值
- 把结果写回待创建的
vals
源码注释里有一句很关键:
computed stored fields with a column have to be computed before create so that required and constraints can be applied on those fields.
这句话基本把它的核心价值说透了:
- 不是为了“更优雅”
- 而是为了让某些存储字段在真正
INSERT前就能参与校验和约束
二、最容易忽略的一条:默认值会直接让 precompute 失效
这点是最容易踩坑的。
fields.py 里明确警告:
- 只有在
create()没有显式提供值 - 并且这个字段也没有 default 值时
- precomputation 才会发生
也就是说,只要下面任一情况存在:
vals里手工给了这个字段- 字段上定义了 default
- 某条 create 链路间接把默认值塞进来了
那么即便你写了 precompute=True,它也可能根本不会在你以为的时机执行。
这就是为什么有些开发者会说:
我明明开了 precompute,怎么像没生效一样?
很多时候不是框架失效,而是默认值先占位了。
三、为什么 Odoo 还特意提醒:precompute 可能适得其反
这又是一个很反直觉的点。
在字段说明里,Odoo 明确写了警告:如果记录是一条一条创建,precompute=True 可能反而比普通 stored compute 更差。
原因并不复杂:
普通 stored compute 的优势
如果不 precompute,很多字段会在后续 flush() 时批量重算。
而批量重算的优势是:
- 可以成批处理记录
- 可以利用 prefetch
- 更容易减少碎片化计算和查询
precompute 的代价
如果你把字段改成 precompute,而你的创建方式又是单条单条进来,那么框架就可能在 create 前对每条记录分别计算一次。
这样一来:
- 批处理优势变弱
- prefetch 价值也被削弱
- 原本可以在 flush 时成批完成的工作,被提前拆散了
所以 precompute=True 并不自动等于“更高性能”。
它更像是:
- 用更早换更可控
- 但未必用更早换来更快
四、它真正更适合的场景:One2many 行由 ORM 批量生成
官方文档注释里专门举了一个更适合的方向:
one2many 的行,往往是通过对父记录写入时,由 ORM 成批创建的。
这时 precompute 更容易发挥价值,因为:
- 虽然是 create 前计算
- 但底层仍然是成批构建 line 记录
- 不会像“外部循环每次 create 一条”那样把计算完全拆碎
这也是为什么很多行级字段、辅助统计字段、或依赖同一批上下文的字段,在 one2many 子表场景里比在主对象场景里更适合考虑 precompute。
五、precompute 和 editable/inverse 字段也不是一回事
在 test_orm 里,官方专门测了多种组合:
precompute=True, store=True, readonly=Trueprecompute=True, store=True, readonly=Falseprecompute=True再叠加inverse- precompute 字段之间相互依赖
这些测试说明:precompute 并不是“把 compute 字段改成另一类字段”,它仍然只是影响何时先求值。
你仍然要分别考虑:
- 字段是否可编辑
- 用户输入是否会覆盖它
- 是否存在 inverse
- 它依赖的字段本身是否也适合 precompute
尤其在测试里,Odoo 还会检查 precompute 依赖链是否一致。因为如果一个 precompute 字段依赖另一个不会在插入前准备好的字段,整个时机设计就容易失去意义。
六、开发里最常见的三种误用
误用 1:把 precompute 当成“所有 compute 字段都该开的优化项”
不对。
只有那些:
- 在插入前就能稳定算出
- 确实需要插入前可用
- 并且不会因为单条创建而失去批量优势
的字段,才值得认真考虑。
误用 2:字段带 default,还期望 precompute 一定生效
这是最常见的“看起来没生效”。默认值本身就可能把 precompute 绕开。
误用 3:把统计型、搜索型字段也贸然 precompute
如果字段计算依赖:
search()_read_group()- 跨表统计
- 大量历史数据
那么把它提前到 create 前,不一定是件好事。你可能只是把重活更早地拆到了每次创建入口上。
七、一个更实用的判断框架
在考虑是否给字段加 precompute=True 时,可以先问四个问题:
1)这个值是否必须在 insert 前拿到?
如果不是,只是“早一点也行”,那不一定值得。
2)这个字段是否容易被 default 或显式值覆盖?
如果是,那你需要先确认实际链路里它到底还有没有触发机会。
3)这个字段的计算是否适合单条执行?
如果不适合,那 precompute 可能是在主动放弃 flush 阶段的批量收益。
4)这个字段是否主要出现在 one2many 批量创建链里?
如果是,precompute 的可行性通常会更高。
结语
precompute=True 的重点,从来不是“把 compute 提前一点就会更快”,而是:
- 让某些存储计算字段在 create 前可用
- 让 required、约束和部分落库逻辑更早拿到结果
- 但同时承担默认值覆盖和单条创建拆碎计算的代价
所以它最该被理解成一个创建时机控制工具,而不是一个盲开的性能选项。
如果你只记住一句话,那就是:
precompute 解决的是“值要不要在插入前就准备好”,不是“字段是不是越早算越好”。
DISCUSSION
评论区