ORM 缓存

Odoo 的 @api.depends_context 不是装饰器糖衣:上下文依赖怎样进入缓存键与重算链

很多开发者知道 @api.depends,却低估了 @api.depends_context 的分量。它不是给代码加点语义说明,而是直接决定字段缓存怎样按上下文分桶、何时必须重算,以及为什么同一条记录会在不同语言、公司或用户下读出不同结果。

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

很多人第一次见到 @api.depends_context,会把它理解成一种“更高级的 depends 注释”。

这理解不算全错,但远远不够。

如果你顺着 /home/ubuntu/odoo-temp/odoo/orm/decorators.pyodoo/orm/fields.pyodoo/orm/environments.py 往下看,会发现它真正做的事是:

告诉 ORM:这个字段的值,不只依赖记录字段,还依赖某些 context 键;所以缓存和重算都必须把这些上下文一起算进去。

也就是说,它不是“写给人看的提示”,而是直接进入框架行为。

一、为什么普通 @api.depends 不够

@api.depends('field_a', 'field_b') 描述的是:

  • 这个计算字段依赖哪些数据库字段
  • 哪些字段变化后要把它标脏并重算

但有一类字段,值的变化并不是由业务字段改动触发,而是由当前上下文触发,比如:

  • 当前语言 lang
  • 当前公司 company / company_id
  • 当前用户 uid
  • 当前目标币种、日期等临时参数

最典型的例子就是汇率、显示名称、翻译后的视图结构、附件二进制大小显示等。

这些值可能在同一条记录、同一时刻、不同上下文里都不一样。

如果你只写 @api.depends,ORM 会以为:

  • 只要记录没变,值就该一样

这时缓存就会把“上一个上下文算出来的值”错当成“所有上下文通用的值”。

二、depends_context 真正影响的是缓存键

fields.py 里,字段会收集两类依赖:

  • depends
  • depends_context

随后这些上下文依赖会被放进 registry 的 field_depends_context 中,由 environment 在缓存时参与 key 计算。

这个设计非常关键。

因为 Odoo 的字段缓存不是“字段名 + 记录 id”这么简单。对于声明了 depends_context 的字段,缓存实际上还要区分:

  • 这是哪种语言算出来的
  • 这是哪家公司上下文算出来的
  • 这是哪个用户视角算出来的
  • 以及你显式声明的其他 context 键

所以可以把它理解成:

@api.depends_context 的本质,是让“同一个字段”在缓存里拥有多套上下文分桶。

不声明,就会串桶;声明了,ORM 才知道该分桶。

三、为什么漏写后最常见的症状是“偶发错值”

这类 bug 最烦人的地方在于,它常常不是每次都错,而是:

  • 管理员看起来正常
  • 普通用户偶尔不对
  • 切英文后又正常
  • 切回中文还是旧值
  • 多公司来回切后结果飘忽

原因不是玄学,而是缓存复用了不该复用的值。

举个直白的理解方式:

  • 第一次在 lang=zh_CN 下算出值 A
  • 结果被缓存
  • 第二次你在 lang=en_US 下读同一字段
  • 如果没声明 depends_context('lang')
  • ORM 可能直接把 A 当缓存命中返回

于是你就得到一个“逻辑正确、时机错误”的旧值。

这种问题特别像脏缓存,所以很多人会先怀疑:

  • 浏览器缓存
  • QWeb 缓存
  • nginx
  • workers 不同步

但根因其实在字段缓存建模阶段就埋下了。

四、哪些 context 键最值得警惕

从 Odoo 自身源码里看,最常见、也最容易漏的是这几个:

1. lang

只要结果会受翻译、显示名、本地化文本影响,就要高度警惕。

ir.ui.view 里一些 arch 读取逻辑,天然就与语言、翻译开关相关。

2. company / company_id

多公司场景里,同一字段值、同一规则、同一汇率语义都可能因公司而变。

res.currency 里就大量依赖公司上下文。

3. uid

如果字段结果和“当前是谁在看”有关,比如权限过滤、个性化显示、用户相关标识,漏写 uid 很容易在管理员与普通用户之间串结果。

4. 临时业务参数

例如:

  • date
  • to_currency
  • bin_size
  • 你自己模块里塞进 context 的业务键

只要字段读值会显式使用它们,就要考虑是否声明。

五、它不只是影响“读缓存”,也影响重算传播

depends_context 很容易被误以为只影响读取缓存,其实它也影响框架对字段依赖关系的理解。

在 registry 收集依赖时,字段除了普通 depends 图,还会额外记录 context 依赖。

这意味着 Odoo 会知道:

  • 这个字段不是纯记录内函数
  • 不能把某一次上下文下的结果当成全局稳定值

换句话说,它不是简单的“缓存失效提示”,而是在告诉 ORM:

这是一个带上下文维度的计算字段。

六、开发里最容易犯的 4 个误区

误区 1:只要字段是 compute,就一律写 @api.depends

错。

有些字段的差异不是字段变化引起,而是上下文变化引起。只写 depends,描述不完整。

误区 2:context 是临时参数,不会影响缓存

恰恰相反。只要计算结果读了 context,它就已经参与了“值是否可复用”的判断。

误区 3:先不写,反正测出来没问题

这种 bug 在单用户、单公司、单语言开发环境里非常容易潜伏。

一到:

  • 多 worker
  • 多公司
  • 多语言
  • 多角色

就会暴露。

误区 4:所有 context 键都一股脑声明进去

也不对。

声明过多会让缓存分桶变碎,降低复用率。真正应该声明的是:

  • 确实参与计算结果的键
  • 而不是上下文里“顺手带着”的所有键

七、实战里怎么判断该不该写

可以用一个很实用的问题来判断:

如果同一条记录,在不同 context 下允许得到不同正确结果,那这个 compute 字段就该认真评估 @api.depends_context

再具体一点,检查这几件事:

  1. 计算函数里有没有 self.env.context.get(...)
  2. 有没有依赖 env.companyenv.user、语言、翻译状态
  3. 有没有调用那些本身依赖 context 的下游方法
  4. 结果是否允许在不同公司 / 语言 / 用户之间变化

如果答案是“有”,那多半不该只停在 @api.depends

八、一句话记忆

可以把 @api.depends@api.depends_context 分成两句话来记:

  • @api.depends哪几个字段变了,我要重算
  • @api.depends_context哪几个上下文变了,同一字段也不能复用旧值

前者描述“数据依赖”,后者描述“视角依赖”。

结语

@api.depends_context 最容易被低估的地方在于:它看起来像语法补充,实际上却在改 ORM 对字段值稳定性的定义。

你一旦理解它控制的是:

  • 缓存键怎么分桶
  • 结果能不能跨上下文复用
  • 字段为什么会在不同语言/公司/用户下读出不同值

很多“偶发错值”“切语言后不刷新”“多公司读到旧结果”的问题,就会从玄学变回工程问题。

而这,正是它真正值钱的地方。

DISCUSSION

评论区

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