Odoo 开发

Odoo 的 precompute 不是“更早算一下”:create 前计算、默认值干扰与批量收益边界讲透

很多人把 precompute 理解成“计算字段提前算”,但官方源码真正强调的是:它只在特定前提下生效,而且并不总能提升性能。本文结合 fields.py、models.py 与 test_orm,讲清 precompute 适合什么、又最容易被什么条件悄悄禁用。

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

在 Odoo 里,很多人第一次看到 precompute=True,会很自然地把它理解成:

这个字段会“更早一点”被 compute,一般应该更快。

这个理解只对了一半。

/home/ubuntu/odoo-temp/odoo/orm/fields.pyodoo/orm/models.py 的源码,以及 addons/test_orm/tests/test_fields.py 里的测试来看,precompute 不是一个泛用性能开关,而是一个严格限定在 create 前阶段的机制。它有明确收益,也有非常容易被忽略的代价。

一、precompute 真正解决的,不是“更快”,而是“插入前必须有值”

fields.py 的字段参数说明里,Odoo 对 precompute 写得很直白:

  • 它表示字段会在记录插入数据库前被计算
  • 适用于那些在插入前就可以安全算出的值
  • 常见动机包括:避免插入后再额外查询、让 required/constraints 更早拿到值

这意味着它首先是一个时机控制机制,而不是一个抽象的“性能优化标签”。

尤其在 models.py_add_precomputed_values() 里,框架会:

  1. 找出模型上所有 precompute=True 的字段
  2. 对缺失这些字段值的 vals 构造 new() 记录
  3. 直接访问这些字段,让 compute 在插入前得到值
  4. 把结果写回待创建的 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=True
  • precompute=True, store=True, readonly=False
  • precompute=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

评论区

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