先说结论
Odoo 的计算字段不是“系统会一直帮你自动刷新”的魔法字段,而是一套很明确的分工:
compute负责怎么算inverse负责怎么把结果写回源字段store决定算出来的值要不要落库precompute决定新记录在插入前,能不能提前算一轮- 重算队列 负责在依赖变化后,把需要更新的记录排进去
所以,真正的理解方式不是“字段有没有自动更新”,而是:
依赖变了以后,Odoo 有没有把它标记成待重算; 读写时有没有触发计算; 用户改计算字段时,能不能通过 inverse 回写到源数据。
一条最常见的定义长这样
total = fields.Float(
compute="_compute_total",
inverse="_inverse_total",
store=True,
precompute=True,
)
这几项分别表示:
compute:total不靠数据库直接存值,而是由_compute_total()算出来inverse:如果用户直接改total,系统要知道怎么反推回源字段store=True:结果会写进数据库,不只是停留在缓存里precompute=True:在条件允许时,新增记录可以先算再插入,减少后续补算成本
一个很容易踩的坑是:
compute不是“保存后自动执行一次”这么简单inverse也不是“装饰性字段参数”,它决定了这个计算值能不能被编辑precompute对非存储字段没有实际意义
源码里也明确做了这个校验:非存储计算字段即使写了 precompute,也不会真正起作用。
depends 不是给人看的,是给重算系统用的
计算字段真正会不会被重算,核心看依赖关系。
在 odoo/orm/fields.py 里,get_depends() 会把依赖来源整理出来:
- 显式写在字段参数里的
depends @api.depends()装饰器收集到的依赖- related 字段的链路依赖
- 以及上下文依赖
depends_context
这意味着,Odoo 并不是简单地“看到 compute 就重算”,而是先把依赖图建出来。
尤其要注意两类情况:
1)链路依赖
比如:
@api.depends('line_ids.price_subtotal')
这里不是只看 line_ids,而是沿着 line_ids -> price_subtotal 这条路径去追踪。
2)可搜索性
源码里会检查依赖链上的中间字段是不是可搜索。原因很现实:
如果系统都不知道哪些记录受影响,就没法高效决定重算谁。
所以有时候你明明写了 depends,字段却“看起来没按预期重算”,问题不一定在 compute 函数,而可能在依赖链设计太绕、或者中间字段无法定位受影响记录。
重算不是立刻跑完,而是先进入队列
Field.recompute() 的逻辑很说明问题:它先从事务里的 tocompute 队列取出待处理记录,再决定要不要计算。
这带来一个很重要的认知:
计算字段的更新,很多时候是“先标记,再统一处理”,不是“每次改一个源字段就当场把所有下游字段全算完”。
这样做的好处是:
- 可以批处理,减少重复计算
- 可以合并同一字段的多次变更
- 可以避免递归依赖把系统拖进无限重算
源码里对递归字段还专门做了逐条处理:
- 普通计算字段可以批量算
- 递归计算字段则必须 record by record
这就是为什么一些“看上去很简单”的字段,在大量数据下也会突然变慢:不是 compute 函数本身慢,而是重算链太长。
compute_value() 其实很讲究
compute_value() 做的不只是“调用 compute 方法”。它还会先处理几件关键事:
- 如果字段设置了
compute_sudo,先切到 sudo 环境 - 对存储字段,先从待计算队列里移除当前记录
- 用保护上下文执行 compute,避免重入和循环触发
- 如果计算失败,再把待计算状态加回去
这说明 Odoo 对计算字段的态度很明确:
- 先保证队列状态正确
- 再执行用户定义的计算逻辑
- 出错时要能恢复到“仍待重算”的状态
也就是说,compute 函数本身应该只专注于“怎么计算”,不要混太多副作用。
inverse 的意义:不是反着算,而是把编辑动作接回源字段
很多人第一次接触 inverse 会以为它是“计算函数的反向公式”。其实更实用的理解是:
当用户修改了计算字段本身,Odoo 要知道该把这个变化写回哪里。
determine_inverse() 只是把 inverse 方法统一调起来,真正的价值是:
- 允许用户直接改一个“看起来是计算出来的值”
- 但系统仍然能把改动落回到真正的业务字段上
例如一些金额汇总字段、展示字段、或可编辑的派生值,都依赖这种机制。
如果没有 inverse,字段往往只能读不能写; 有了 inverse,字段才真正成为“可编辑的派生层”。
store 和 precompute 的边界
这两个参数很容易被混淆。
store=True
表示结果要写进数据库。
它带来的好处是:
- 搜索更快
- 分组更方便
- 报表更稳定
- 不必每次读取都现算
代价是:
- 依赖变化时要维护重算链
- 写入成本更高
- 设计不好会产生大量无效重算
precompute=True
表示在合适的时机,系统可以在插入前先把值算出来。
它适合:
- 新记录依赖很明确
- 计算结果在创建时就需要用到
- 希望减少后续 flush/recompute 压力
但它不是万能提速开关。官方源码里也明确提示:
- 只有 stored computed field 才谈得上 precompute
- 如果依赖上游本身不是 precompute 友好的,当前字段也会被迫降级
所以,precompute 更像“早算”的机会,不是“必须早算”的承诺。
实战里最常见的三个误解
误解 1:计算字段为什么没立刻变
很多时候不是没变,而是:
- 值还在缓存里
- 队列还没 flush
- 当前事务里还没触发读取
要先分清“缓存值”“数据库值”“待计算状态”这三层。
误解 2:store=True 就一定更快
不一定。
如果依赖很宽、变化很频繁,store=True 只是把成本从“读时计算”转成“写时重算”。
误解 3:inverse 是可有可无
如果一个派生字段希望支持手工编辑,inverse 往往是必须的。否则你会得到一个看得见、改不动、又容易让用户误解的字段。
一个更实用的设计建议
如果你要设计自己的计算字段,可以先问自己四个问题:
- 这个字段是只读展示,还是需要用户编辑?
- 是否真的需要落库?
- 依赖链是否足够短、足够可搜索?
- 是否值得提前计算,还是让它在读取时现算更稳妥?
这四个问题想清楚,很多计算字段设计问题基本就能避免。
总结
把 Odoo 计算字段记成一句话就够了:
compute决定怎么算,inverse决定怎么回写,store决定要不要落库,precompute决定能不能提前算。
真正难的不是写出一个 compute 函数,而是把依赖图、缓存、重算队列和回写逻辑设计得刚刚好。
English sidecar
The short version
An Odoo computed field is not a magical “always auto-refresh” field. It is a small contract with four parts:
computedefines how the value is calculatedinversedefines how to write a user edit back to source fieldsstoredecides whether the result is persisted in the databaseprecomputedecides whether Odoo may compute the value before insert
The real flow is:
a dependency changes → Odoo marks records for recompute → cache/flush logic runs →
computeis called when needed → if the field is editable,inversepushes edits back to source data
A typical definition
total = fields.Float(
compute="_compute_total",
inverse="_inverse_total",
store=True,
precompute=True,
)
Meaning:
compute:totalis derived, not directly typed into a columninverse: user edits can be mapped back to the real source fieldsstore=True: the result is stored in the databaseprecompute=True: if conditions are right, Odoo may compute it before insert
A common mistake is to treat these as cosmetic options. They are not.
Dependencies drive recomputation
@api.depends() and the field-level depends parameter are not for documentation only. Odoo uses them to build the dependency graph.
That graph can include:
- explicit dependencies from the field definition
- dependencies collected from
@api.depends() - related-field chains
- context dependencies through
depends_context
So Odoo does not simply see a compute method and recompute blindly. It first needs to know which records are affected.
This is why chained dependencies and searchable intermediate fields matter.
Recomputation is queued, not always immediate
In Field.recompute(), Odoo reads pending work from the transaction’s tocompute queue.
That means computed values are often handled as:
- mark records as dirty
- batch work later
- avoid repeated recomputation
- prevent recursive loops from exploding
Recursive computed fields are handled record by record, while normal ones can be batched.
What compute_value() actually does
compute_value() is more than “call the method”. It also:
- switches to sudo if
compute_sudois enabled - removes stored fields from the to-compute queue first
- protects the computation from re-entrancy
- restores the queue if the computation fails
So a compute method should stay focused on calculation, not on side effects.
inverse is about write-back, not reverse math
inverse is often misunderstood. It is not necessarily a mathematical inverse.
Its real job is to tell Odoo:
“If the user edits this computed value, here is where that change should go.”
That is what makes a computed field editable instead of read-only.
store vs precompute
store=True
The value is persisted.
Pros:
- faster search and grouping
- stable reporting
- less work on every read
Cons:
- recomputation cost on writes
- dependency maintenance overhead
precompute=True
Odoo may compute the value before insert when possible.
This is useful when the field is needed right away on new records, but it is not a universal optimization switch.
It only matters for stored computed fields, and upstream dependencies may still force a fallback.
Practical takeaway
When designing a computed field, ask:
- Is it read-only display or user-editable?
- Does it really need to be stored?
- Is the dependency chain short and searchable?
- Should it be precomputed, or is on-demand computation safer?
If you answer those clearly, most computed-field problems disappear before they start.
DISCUSSION
评论区