先说结论
在 Odoo 里,很多人把 self.env 用得很熟,却没真正分清它和下面两层的关系:
cursor (cr)registryEnvironment (env)
最容易记住的一句话是:
- cursor 负责当前数据库事务通道
- registry 负责当前数据库有哪些模型类
- env 负责把“当前事务 + 当前用户 + 当前上下文 + ORM 缓存”打包成开发时真正拿来用的运行环境
所以:
env 不是数据库连接,也不是模型注册表本身,而是你当前这次 ORM 操作的“工作语境”。
官方源码怎么定义 Environment
在 /home/ubuntu/odoo-temp/odoo/orm/environments.py 开头,Environment 的注释已经写得很清楚。
它存的是:
cr: 当前数据库 cursoruid: 当前用户 idcontext: 当前上下文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.partnerrecordset 入口。
所以同一个模型名,在不同 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 cache、protected等也都来自 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_company、default_get / context、NewId / _origin 等文章。
而本文的切入点更底层:
- env 本体是什么
- registry 和 cursor 分别处在哪一层
- 为什么 env 不是全局单例,也不是可变对象
也就是从ORM 运行时容器结构去讲,而不是从某个具体 API 技巧去讲。
最后一句话
很多 Odoo 开发的“玄学感”,其实来自把 env 想得过于模糊。
而官方源码真正想表达的是:
env 不是一个随手可改的小工具,它是把事务、身份、上下文、模型目录和缓存机制绑在一起的 ORM 工作环境。
这层想清楚后,很多 recordset、权限和上下文问题都会一下子顺很多。
DISCUSSION
评论区