Odoo 开发

Odoo browse 不会帮你查库:browse、exists 和“幽灵 recordset”到底怎么理解

很多人把 browse 当成“按 id 查一条记录”,但官方源码里它其实只是把 id 包装成 recordset。也正因为如此,browse 出来的对象不等于数据库里一定存在。本文讲清 browse、exists、NewId 和删除后 recordset 的真实边界。

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

很多 Odoo 开发者会把下面这句代码读成:

partner = self.env['res.partner'].browse(42)

去数据库里查 id=42 的 partner。

但这其实不是 browse() 的真实语义。

/home/ubuntu/odoo-temp/odoo/orm/models.py 里的实现看,browse() 做的事情非常克制:

  • 把传入的 id 规范成 tuple
  • 构造一个当前模型、当前环境下的 recordset
  • 直接返回

它并没有在 browse() 这一刻就验证数据库里是否真有这条记录。

这就是很多“看起来诡异”的 ORM 行为的根源。

一、browse 的本质:包装 id,不是立即验真

源码里的 browse() 非常短,几乎没有额外逻辑。它接受:

  • 单个 id
  • 一组 id
  • 空值

然后返回一个 recordset。

这说明 browse() 的职责更像:

在当前 env 中声明“我现在要以这些 id 作为一组记录来继续操作”。

而不是:

立刻帮我确认这些记录是否都存在。

所以你可以 browse(999999),代码本身并不会因为数据库里没有这个 id 就立刻报错。

二、为什么会出现“幽灵 recordset”

所谓“幽灵 recordset”,就是:

  • Python 层已经有一个 recordset 对象
  • 但它指向的某些 id 在数据库里并不存在,或者已经被删了

这不是 bug,而是 ORM 语义的一部分。

最典型的几种情况:

1)手工 browse 一个不存在的 id

rec = self.env['x.model'].browse(999999)

rec 这个对象是存在的,但底层记录不一定存在。

2)你先拿到记录,后来它被删了

比如:

  • 你先保存了 rec
  • 另一个链路删掉了对应行
  • 你手里的 recordset 还在

这时你再继续操作它,很多行为会和“真实存在记录”不一样。

3)new 记录和 NewId

在 create 前、onchange 中,Odoo 还会构造带 NewId 的临时记录。这些对象也不是数据库里的真实行,但在 ORM 里又必须被当成“当前逻辑中的记录”处理。

三、exists() 才是“这批记录现在还真在吗”的过滤器

models.py 里,exists() 做的事情才接近很多人对 browse 的想象。

它会:

  1. self._ids 里的 NewId 和真实 id 分开
  2. 对真实 id 生成 SQL 查询,检查哪些 id 仍然存在
  3. 把“确实存在的真实 id”与 NewId 合并
  4. 返回存在子集组成的新 recordset

这里有两点非常关键。

1)exists() 会保留 NewId

源码注释明确说:

By convention, new records are returned as existing.

也就是说,ORM 认为:

  • 数据库里还没插入的新记录
  • 在当前逻辑上下文里依然算“存在”

这不是数据库意义上的存在,而是 ORM 生命周期意义上的存在。

2)exists() 返回的是子集 recordset,不是布尔值

很多人会写:

if rec.exists():
    ...

这当然可以,因为空 recordset 在布尔上下文里会是假。

但更准确地理解应该是:

  • exists() 不是在回答 yes/no
  • 它是在把当前 recordset 过滤成“仍可继续操作的那部分”

这就是为什么它尤其适合批量 recordset,而不只是单条记录判断。

四、为什么这件事对开发很重要

场景 1:不要把 browse 当成权限和存在性校验

如果外部传来一个 id,你直接:

record = self.env['x.model'].browse(record_id)

这不等于你已经安全拿到了真实对象。

你可能还需要进一步:

  • record.exists()
  • 权限检查
  • 业务状态检查

否则你只是在拿一个“声明式引用”。

场景 2:删除后旧 recordset 不是自动消失

很多人会下意识觉得,记录删掉后,之前的 Python 对象就应该“失效”。

但 ORM recordset 不是实时代理,它只是包了一组 id 和 env。删库不会自动让你手里的变量变成 None

所以删除后继续复用旧 recordset 时,要有“这可能已经是幽灵引用”的意识。

场景 3:批量处理时,exists() 可以顺手清洗集合

例如:

records = (a + b + c).exists()

这类写法的价值,不只是“判断存在”,而是把后续流程统一建立在“这批记录确实还活着”之上。

五、最常见的误解

误解 1:browse(id) 等于 search([('id', '=', id)])

不等于。

search() 会真正查数据库并按 domain 过滤;browse() 只是把 id 包装成 recordset。

误解 2:if record: 就说明数据库里有这条记录

也不一定。

recordset 的真假和它是否为空有关,而不是数据库里对应行此刻一定存在。

误解 3:删除后旧对象就自然无效了

也不对。旧 recordset 变量还在,只是它指向的 id 可能已经不存在了。

六、一个更稳的心智模型

理解 browse(),最好换成这样一句话:

browse() 是“按 id 建立 ORM 引用”,不是“按 id 做存在性查询”。

理解 exists(),最好换成:

exists() 是“把当前引用集合收缩成仍然有效的子集”。

有了这个区分,很多 API 行为就容易理解了:

  • 为什么 browse() 很轻
  • 为什么不存在 id 也能先得到 recordset
  • 为什么 exists() 对批量集合很重要
  • 为什么 NewId 会被当成存在

结语

在 Odoo ORM 里:

  • browse() 负责构造引用
  • exists() 负责过滤现实

把这两者混成一个概念,后面就容易在删除、onchange、批量处理、外部传参这些场景里踩坑。

所以更准确的说法不是“browse 查记录”,而是:

browse 先给你一个 recordset 壳子;至于这壳子里包的 id 现在是否真存在,要看 exists() 这一层。

DISCUSSION

评论区

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