Odoo 开发

Odoo 的 _origin 不是“旧值快照”:表单草稿、已存在行与原始记录语义讲透

很多人第一次接触 _origin,会把它误解成“修改前的旧值”或“临时记录对应的数据库镜像”。但从 Odoo 源码看,_origin 真正表达的是记录身份背后的原始记录语义,尤其在 new()、x2many 表单编辑与 onchange diff 回传里非常关键。

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

很多开发者第一次看到 _origin,直觉都会把它理解成下面两种东西之一:

  • “这条记录修改前的旧值”
  • “这个 NewId 背后对应的数据库记录快照”

这两个理解都不够准确。

/home/ubuntu/odoo-temp/odoo/orm/models.pyaddons/web/models/models.py 的实现看,_origin 真正承担的职责是:

在“当前 record 不是纯粹数据库实体”的场景下,给你一个语义上的原始记录身份

它关心的重点不是“字段旧值”,而是“你现在手里的这条记录,最初是从谁演化来的”。

一、new(origin=...) 已经把 _origin 的定位写得很清楚了

在 ORM 的 new() 实现里,Odoo 明确支持:

record = self.new(values, origin=self)

源码注释直接写道:

  • 这是一个只存在于内存里的新记录
  • 如果传了 origin,它会被保存为 record._origin
  • 两个拥有同一 origin 的新记录,会被视为同源

这说明 _origin 的第一层含义不是“旧值集合”,而是:

  • 当前记录是临时记录
  • 但它在语义上并不是凭空长出来的
  • 它背后可能对应一个真实记录来源

所以 _origin 更像“身份溯源指针”,而不是“字段前镜像”。

二、_origin 不是字段回滚机制,它不帮你保存 before/after 差异

这点特别容易误解。

在源码里,_origin 属性做的事情非常克制:

  • 如果当前 self 都是真实 id,就直接返回 self
  • 如果包含 NewId,就通过 OriginIds(...) 把它们映射回真实来源 id
  • 最终返回一个“原始记录 recordset”

注意,它返回的是记录身份集合,不是字段旧值快照。

也就是说:

  • record._origin.name 可能给你原记录当前值
  • 但它并不等于“表单修改前的 name 历史值”
  • 如果原记录后来又被别的逻辑改了,你看到的是当前原记录,不是冻结快照

所以调试时如果你想比较 before/after,不能把 _origin 当成版本系统。

三、它为什么在表单和 onchange 里格外重要

真正让 _origin 变得关键的,是 Web 端的 onchange 管线。

addons/web/models/models.py 里,Odoo 处理表单 onchange 时会:

  1. 先根据表单当前值构造内存 record
  2. 为这个 record 建 RecordSnapshot
  3. 执行 onchange 和重算
  4. 再把新旧 snapshot 做 diff
  5. 把差异回传给客户端

这里 _origin 的意义就出来了。

对于 x2many 行,如果某一行本来是数据库里已存在的记录,前端编辑时它在内存中可能表现成一个 NewId 包装过的临时行。但在回传 diff 时,Odoo 又必须知道:

  • 这是“新建一行”
  • 还是“链接已有行”
  • 还是“在已有行基础上做 update”

RecordSnapshot.diff() 在处理这类情况时,会显式使用:

base_line = line_snapshot.record._origin

也就是说,框架不是把这条临时行当成全新实体,而是通过 _origin 找回“它原本是谁”,再决定该生成:

  • Command.LINK
  • Command.UPDATE
  • Command.CREATE

这正是 _origin 最有价值的地方:

它让“表单里的临时对象”仍然保留了与真实业务记录之间的身份连续性。

四、为什么很多自定义逻辑会把 _origin 用错

常见误用有三类。

1)把 _origin 当旧值容器

有人会写:

if record.amount_total != record._origin.amount_total:
    ...

这在某些场景下“看起来可用”,但它不是严格意义上的旧值比较机制。因为 _origin 代表的是来源记录,不保证是一个冻结快照。

2)把 _origin 当作“有值就一定是数据库老记录”

也不完全对。

_origin 表达的是来源语义,不是简单的“是否已经真正落库”。在复杂表单链路里,你手里的 record 可能是临时对象,但它仍然能指向某个已有来源。

3)在 onchange / wizard 里混淆 selfself._origin

self 代表当前缓存状态,可能已经叠加了用户尚未保存的新值;self._origin 代表来源记录身份。两者解决的问题不同:

  • self:现在这份草稿长什么样
  • self._origin:这份草稿最初从谁演化而来

搞混这两个对象,很容易让逻辑在表单测试里“看似正常”,上线后却在 x2many 编辑场景里出现莫名其妙的 diff 或更新异常。

五、开发时该把 _origin 当成什么

一个更稳的理解方式是:

_origin 是 Odoo 在“临时记录 / 表单态记录 / NewId 包装记录”里保留下来的身份锚点

它适合回答的问题是:

  • 这条临时记录最初对应哪个真实记录?
  • 当前 diff 应该按 create、link 还是 update 来表达?
  • 我现在看到的是“新对象”,还是“旧对象的一份编辑态外壳”?

不适合直接承担:

  • 历史版本比对
  • 审计日志 before/after
  • 严格的旧值快照存档

结语

理解 _origin 后,你会发现它不是一个“顺手加上的辅助属性”,而是 Odoo 为表单态 ORM 设计的一根关键骨架。

它保证了这样一件事:

  • 即便你现在操作的是内存里的临时记录
  • 框架仍然知道它和哪个真实业务记录有连续关系

所以 _origin 的重点从来不是“旧值”,而是身份连续性

一旦把这层意思想明白,很多 onchange、x2many diff、表单调试里的怪现象都会突然顺起来。

DISCUSSION

评论区

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