env 三层边界

Odoo 里的 env 到底是什么:Environment、registry、cursor 三层边界讲透

很多 Odoo 开发天天写 self.env,却常把 env、registry、cursor 混成一团。本文结合官方 ORM 源码,讲清三者分别负责什么、为什么 env 不是“全局单例”。

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

先说结论

在 Odoo 里,很多人把 self.env 用得很熟,却没真正分清它和下面两层的关系:

  • cursor (cr)
  • registry
  • Environment (env)

最容易记住的一句话是:

  • cursor 负责当前数据库事务通道
  • registry 负责当前数据库有哪些模型类
  • env 负责把“当前事务 + 当前用户 + 当前上下文 + ORM 缓存”打包成开发时真正拿来用的运行环境

所以:

env 不是数据库连接,也不是模型注册表本身,而是你当前这次 ORM 操作的“工作语境”。


官方源码怎么定义 Environment

/home/ubuntu/odoo-temp/odoo/orm/environments.py 开头,Environment 的注释已经写得很清楚。

它存的是:

  • cr: 当前数据库 cursor
  • uid: 当前用户 id
  • context: 当前上下文
  • su: 是否超级用户模式

并且它还:

  • 提供从模型名到 model 的映射接口
  • 持有记录缓存
  • 管理 recomputation 相关结构

这其实已经告诉你一个非常关键的事实:

env 是 Odoo ORM 的运行时容器,不是一个简单参数包。

它既带安全语义,也带上下文语义,还带缓存和重算协作语义。


self.env['res.partner'] 为什么能直接拿模型

Environment 实现了 Mapping 接口。

源码里的 __getitem__() 会:

  • 通过 self.registry[model_name]
  • 返回一个绑定当前 env 的空 recordset

也就是说,当你写:

self.env['res.partner']

你拿到的并不是“某个全局 model 类”,而是:

当前 env 语义下的 res.partner recordset 入口。

所以同一个模型名,在不同 env 下意义并不完全相同,因为:

  • 用户可能不同
  • context 可能不同
  • superuser 模式可能不同
  • 事务缓存也可能不同

这就是为什么 Odoo 一直强调“recordset 绑定 env”。


env 为什么不是全局单例

Environment.__new__() 的实现特别值得看。

源码逻辑大致是:

  • 先从 cr.transaction 找当前 transaction
  • 如果没有,就基于 Registry(cr.dbname) 建一个 Transaction
  • 然后检查 transaction 里是否已有同一组 (cr, uid, su, context) 的 env
  • 如果有,就直接复用
  • 没有才新建

这说明 env 既不是“每次都新建”,也不是“全局单例”。

更准确地说:

env 是在当前 transaction 范围内,按 (cursor, user, su, context) 复用的运行环境对象。

这比“全局单例”细得多,也比“每次都重建”高效得多。


env 为什么是只读的

源码里 __setattr__() 有一条很直接的限制:

  • 一旦初始化完成
  • 已有属性就不能再改
  • 会提示你应该用 env() 来生成新环境

这其实是一个非常聪明的设计。

因为如果 env 本身可以随手改:

  • 你今天改了 user
  • 明天改了 context
  • 后天又在原对象上切 su

整个 recordset 世界会很难保证一致性。

而 Odoo 的思路是:

  • 环境不可变
  • 想换参数,就克隆出一个新 env

所以 with_context()sudo()with_user() 这些写法的哲学根子都在这里:

不是原地修改环境,而是在新语境里继续工作。


env() 到底在做什么

Environment.__call__() 允许你基于当前 env 派生新 env:

  • 可以换 cr
  • 可以换 user
  • 可以换 context
  • 可以换 su

最终还是回到 Environment(cr, uid, context, su)

这说明 env 的“切换”本质上不是魔法,而是:

从旧环境派生一个参数不同、但仍符合复用规则的新环境。

这也是为什么很多 API 看起来像:

  • env(user=...)
  • self.with_context(...)
  • self.sudo()

它们底层都是在改“语境”,不是在改“模型定义”。


registry 又处在哪一层

/home/ubuntu/odoo-temp/odoo/orm/registry.py 里,Registry 的定义也很清楚:

  • 每个数据库一个 registry 实例
  • 它本质上是模型名到模型类的映射

Registry.__new__() 会按 db_name 复用已有 registry。

也就是说:

  • registry 更偏“数据库级模型目录”
  • env 更偏“当前事务级工作环境”

所以二者不是上下位的简单替代关系,而是两个不同层级:

registry 负责回答

  • 这个数据库里有哪些模型
  • 当前模型类定义是什么
  • 模块加载后模型目录如何组织

env 负责回答

  • 当前用谁的身份在操作
  • 当前上下文是什么
  • 当前事务缓存是什么
  • 当前 recordset 应该怎样解释

cursor 又为什么不能等同于 env

很多新手会把 self.env.cr 看成“env 的数据库部分”,然后脑子里默认两者差不多。

其实差很多。

cursor 更像:

  • 当前事务的 SQL 通道
  • 负责执行查询、commit、rollback、savepoint

但 cursor 本身并不知道:

  • 当前用户是谁
  • 当前 context 是什么
  • 哪个 model 名对应哪个类
  • ORM cache 该怎么组织

这些事情是 env / transaction / registry 协作出来的。

所以:

你可以说 env 持有 cursor,但不能说 cursor 就等于 env。

这就像:

  • cursor 是路
  • registry 是地图册
  • env 是“带着身份、上下文和缓存上路的你”

为什么 env 和 transaction 绑得这么紧

源码里还能看到:

  • env 会拿到 transaction.registry
  • cacheprotected 等也都来自 transaction

这意味着 Odoo 并不是把缓存散落在 recordset 身上,而是把很多运行时状态放在当前 transaction 级别统一管理。

这就解释了很多实战现象:

  • 同一事务里读过的数据,后续可能命中缓存
  • 切 context / user 后,虽然是新 env,但仍可能共享部分 transaction 级结构
  • 同一事务结束后,这套运行时状态也会跟着结束或重置

所以 env 的真正语义,绝不是“一个方便取模型的小对象”这么浅。

它是 transaction 世界的开发者接口。


新手最容易误解的 4 件事

1. 以为 env 是全局单例

不是。它在当前 transaction 内按参数复用。

2. 以为 self.env['x.model'] 拿到的是全局模型类

不是。拿到的是绑定当前 env 的空 recordset。

3. 以为改 env 属性就能切换上下文

不行。env 设计成只读,要派生新环境。

4. 以为 cr、registry、env 只是同一个东西的不同叫法

不是。三者职责层级完全不同。


实战上最有用的理解方式

如果你要判断某个问题应该看哪一层,可以这样想:

SQL / 事务问题

先看 cr

比如:

  • savepoint
  • rollback
  • 手写 SQL

模型是否存在 / 模块是否加载

先看 registry

比如:

  • 模型类有没有注册进来
  • 模块升级后 registry 有没有重建

权限、上下文、缓存、recordset 行为

先看 env

比如:

  • 为什么 sudo() 后结果变了
  • 为什么 with_context() 后字段或默认值不同
  • 为什么同一模型在不同调用点行为不一样

这比把所有异常都模糊地归到“env 有问题”要有效得多。


和现有文章怎么区分

站里已有关于 sudo / with_user / with_companydefault_get / contextNewId / _origin 等文章。

而本文的切入点更底层:

  • env 本体是什么
  • registry 和 cursor 分别处在哪一层
  • 为什么 env 不是全局单例,也不是可变对象

也就是从ORM 运行时容器结构去讲,而不是从某个具体 API 技巧去讲。


最后一句话

很多 Odoo 开发的“玄学感”,其实来自把 env 想得过于模糊。

而官方源码真正想表达的是:

env 不是一个随手可改的小工具,它是把事务、身份、上下文、模型目录和缓存机制绑在一起的 ORM 工作环境。

这层想清楚后,很多 recordset、权限和上下文问题都会一下子顺很多。

DISCUSSION

评论区

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