很多开发者第一次看到 _origin,直觉都会把它理解成下面两种东西之一:
- “这条记录修改前的旧值”
- “这个 NewId 背后对应的数据库记录快照”
这两个理解都不够准确。
从 /home/ubuntu/odoo-temp/odoo/orm/models.py 与 addons/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 时会:
- 先根据表单当前值构造内存 record
- 为这个 record 建
RecordSnapshot - 执行 onchange 和重算
- 再把新旧 snapshot 做 diff
- 把差异回传给客户端
这里 _origin 的意义就出来了。
对于 x2many 行,如果某一行本来是数据库里已存在的记录,前端编辑时它在内存中可能表现成一个 NewId 包装过的临时行。但在回传 diff 时,Odoo 又必须知道:
- 这是“新建一行”
- 还是“链接已有行”
- 还是“在已有行基础上做 update”
RecordSnapshot.diff() 在处理这类情况时,会显式使用:
base_line = line_snapshot.record._origin
也就是说,框架不是把这条临时行当成全新实体,而是通过 _origin 找回“它原本是谁”,再决定该生成:
Command.LINKCommand.UPDATE- 或
Command.CREATE
这正是 _origin 最有价值的地方:
它让“表单里的临时对象”仍然保留了与真实业务记录之间的身份连续性。
四、为什么很多自定义逻辑会把 _origin 用错
常见误用有三类。
1)把 _origin 当旧值容器
有人会写:
if record.amount_total != record._origin.amount_total:
...
这在某些场景下“看起来可用”,但它不是严格意义上的旧值比较机制。因为 _origin 代表的是来源记录,不保证是一个冻结快照。
2)把 _origin 当作“有值就一定是数据库老记录”
也不完全对。
_origin 表达的是来源语义,不是简单的“是否已经真正落库”。在复杂表单链路里,你手里的 record 可能是临时对象,但它仍然能指向某个已有来源。
3)在 onchange / wizard 里混淆 self 与 self._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
评论区