很多开发者第一次看到 @tools.ormcache(...),会把它理解成“给一个方法加缓存”。这话不算错,但如果只停在这一层,后面就很容易踩坑。
在 Odoo 里,ormcache 不是一个随便装饰一下就结束的小技巧。它和 registry 级缓存容器、事务内 lookup 统计、上下文和用户边界、缓存失效时机 都紧密相关。更关键的是:它缓存的值如果选错,问题不会马上爆,而是会在后面的请求里以“偶发错乱”的形式出现。
这篇就沿着官方源码,把 ormcache 真正该怎么理解讲清楚。
一、先看源码:ormcache 到底缓存到哪里
在 /home/ubuntu/odoo-temp/odoo/tools/cache.py 里,ormcache.lookup() 的核心逻辑很直接:
- 先根据装饰器参数生成 key
- 再去
model.pool._Registry__caches[self.cache_name]里取值 - 命中就直接返回
- miss 才真正执行原方法并写回缓存
这里最容易忽略的点是:缓存容器不在 recordset 上,也不在 env 上,而在 registry 上。
这意味着:
- 它不是“这次函数调用内部的临时 memoization”
- 它也不是“某个对象实例私有缓存”
- 它更像“这个数据库 registry 下面可复用的一块 LRU 缓存”
所以它适合缓存的,往往是:
- 配置类结果
- 权限判定结果
- 路由、模板、语言、菜单这类稳定计算结果
- 可通过明确 key 完整描述的纯结果
而不适合缓存:
- 带事务依赖的临时对象
- 强依赖当前 cursor 生命周期的值
- 还会继续懒加载的 recordset
二、为什么官方源码明确说“不要返回 recordset”
cache.py 里有一句很重要的注释:
Methods implementing this decorator should never return a Recordset, because the underlying cursor will eventually be closed...
这句话不是“建议”,而是一个很实战的边界提醒。
recordset 看起来像普通 Python 对象,但它背后绑定的是:
- 当前
env - 当前
cursor - 当前上下文
- 当前预取缓存状态
如果你把一个 recordset 放进 ormcache,下一次命中缓存时,调用方拿到的可能是一个 绑定旧 cursor 的对象。旧事务结束后,这个 recordset 再去读字段、做懒加载、查关联,很可能直接炸出接口错误,或者更隐蔽地拿到错误上下文下的结果。
所以 ormcache 更安全的返回值通常是:
intstrbooltupledict/list(内容本身不再依赖活动 cursor)- ID 集合,而不是 recordset 本身
如果后续确实要操作记录,应该缓存 ids,然后在当前环境里重新 browse(ids)。
三、key 不是装饰器语法糖,而是缓存正确性的核心
ormcache 最核心的能力,其实不是“存”,而是 怎么切 key。
源码里 determine_key() 会把你写进装饰器的参数表达式,和 self._name、方法对象一起拼成一个可执行 key 生成函数。也就是说,下面这种写法:
@tools.ormcache('self.env.uid', 'mode')
def _something(self, mode):
...
本质上是在告诉 Odoo:
- 同一个模型方法
- 不同用户
- 不同 mode
应该被视为不同缓存槽位。
这就是 ormcache 最容易被误用的地方:很多 bug 不是缓存本身错,而是 key 少切了一刀。
常见漏项包括:
- 忘了把
self.env.uid放进去,导致不同用户共用结果 - 忘了把
self.env.lang放进去,导致翻译串语言 - 忘了把
self.env.company.id或tuple(self.env.companies.ids)放进去,导致多公司串值 - 忘了把上下文关键字段放进去,导致同方法在不同上下文下拿到同一缓存
所以判断一个方法能不能上 ormcache,先别问“它慢不慢”,先问:
我能不能用一个稳定、完整、足够小的 key,把结果依赖条件表达清楚?
如果不能,先别缓存。
四、Odoo 19 之后,为什么连 ormcache_context 都在弱化
同一个文件里还能看到 ormcache_context,但源码已经给出明显信号:
skiparg已经过时ormcache_context也被提示逐步让位- 现在更推荐直接在 key 表达式里写
self.env.context.get(...)
这背后的设计思路很清晰:
缓存 key 应该显式,而不是靠“神奇地顺带带上 context”。
这样做有两个好处:
- 读代码的人更容易看懂这个缓存到底依赖什么
- 你不会不小心把整个上下文空间都卷进来,导致 key 爆炸
开发里最稳妥的做法不是“把 context 全带上”,而是只挑真正影响结果的那几个 key,比如:
langtzallowed_company_ids- 某个功能开关上下文
五、缓存命中了,不代表设计就是对的
lookup() 里除了 hit / miss 统计,还有一个容易被忽略的细节:
tx_lookups = model.env.cr.cache.setdefault('_ormcache_lookups', set())
也就是说,Odoo 不只是统计总命中,还会区分 当前事务里第一次 lookup。这说明官方在意的不只是“总体缓存率”,还在意:
- 是不是每个事务都在反复做同类计算
- 缓存有没有真正减少事务内重复工作
但这里要注意一件事:
高命中率不等于高质量缓存。
如果你的 key 切得过粗,命中率可能非常漂亮,但结果已经串用户、串语言、串公司了。缓存系统不会替你判断业务语义是否正确,它只负责按 key 复用结果。
六、什么时候该主动清缓存
只要一个缓存结果依赖的数据变了,就必须考虑失效。
在 Odoo 里,很多官方方法会通过:
- 模型自己的
clear_caches() - registry / signal 机制
- 特定模型写操作后的 cache clear
来控制缓存失效。
开发里如果你自己写了 ormcache 方法,也要同步想清楚:
- 谁会修改这份基础数据?
- 修改后在哪个入口清?
- 是清某个模型缓存,还是整个相关 cache 区?
如果没有配套失效策略,缓存值迟早会变成“稳定返回错误结果”的加速器。
七、实战里最容易犯的 4 个错
1)把它当作“性能万金油”
方法慢,不一定该缓存。先判断问题是:
- SQL 太多
- 循环写法太碎
- 预取没吃到
- 权限域太重
- 还是结果本身真的足够稳定
很多时候,先改批处理写法,比先上缓存更对路。
2)缓存依赖数据库实时状态的复杂对象
例如 recordset、带懒加载对象、依赖当前事务脏数据的结构。这种设计短期可能没问题,长期几乎一定出事故。
3)key 漏掉安全边界
只要结果和用户、公司、语言、上下文相关,key 就不能偷懒。
4)只会加缓存,不会做失效设计
缓存从来不是“写上装饰器就完了”,它是“计算 + 复用 + 失效”三件事一起成立才算完整。
八、一个够实用的判断标准
如果你准备给某个方法加 ormcache,可以先过这 5 个问题:
- 结果是否足够稳定,能跨请求复用?
- 结果能否不用 recordset 表达?
- 结果依赖条件能否被一个明确 key 完整覆盖?
- key 会不会大到失去缓存意义?
- 底层数据变化时,我知道在哪里清缓存吗?
五个问题里有两个答不上来,通常就先别上。
结语
ormcache 的真正价值,不是“让一个函数更快”,而是把 稳定、可描述、可失效 的结果,安全地提升到 registry 级复用。
它的关键从来不在装饰器那一行,而在三件事:
- key 有没有切对
- 返回值是不是安全
- 失效策略有没有补上
把这三件事想明白,你写出来的缓存才是加速器;否则它只是一个更难排查的 bug 放大器。
DISCUSSION
评论区