先说结论
Odoo 里的 One2many / Many2many 并不是“字段值是一个列表”这么简单。
它们真正接受的是一套关系命令协议:
- 新增关联
- 更新关联记录
- 从关系里解绑
- 直接删除关联记录
- 清空整组关系
- 用一批新 id 整体替换旧关系
这套协议在源码里被 odoo/orm/commands.py 的 Command 类统一包装。你在 Python 里看到的是可读的 helper,底层落到数据库前,依旧是三元组。
一句话理解:
x2many 不是“写一个值”,而是“描述一次关系变更”。
七种命令分别在说什么
Command 把七种动作固定成常量:
create(0, 0, values):创建新记录并建立关系update(1, id, values):更新已有关联记录delete(2, id, 0):删除记录本身,再处理关系unlink(3, id, 0):只断开关系link(4, id, 0):把已有记录挂上来clear(5, 0, 0):清空所有关系set(6, 0, ids):用新 id 集合整体替换旧关系
其中最容易混淆的是 delete 和 unlink:
delete想删的是“数据库里的那条记录”unlink想删的是“和当前记录之间的关系”
对 Many2many 来说,这两者差得尤其大。一个是删行,一个是删关联。
One2many 和 Many2many 的 create 语义不一样
Command.create(values) 看起来很简单,但在两种关系里含义不同:
- Many2many:所有当前记录共享创建出来的那一条 comodel 记录
- One2many:每个父记录都会各自创建自己的子记录
这也是为什么同样写一个 Command.create(...),在 O2M 和 M2M 上的结果会不同。
如果你把 x2many 当成“数组 append”,就很容易误判最终数据结构。
为什么官方代码里还会出现 (4, id) 和 Command.set(ids)
在旧代码和某些底层拼值场景里,你会看到:
'move_dest_ids': [(4, x) for x in self.move_dest_ids.ids]
这就是最原始的 link 命令。
而新代码更推荐:
'reference_ids': [Command.set(self.order_id.reference_ids.ids)]
理由很简单:
Command更可读- 不容易把命令号写错
- 代码审查时更容易看懂“你到底想做什么”
set() 的语义也很明确:先移除旧的,再补上新的。它不是“追加”,而是“整体替换”。
最常见的三个坑
1. 以为 unlink 会删掉 Many2many 目标记录
不会。它只会断开关系。目标记录可能还被别的地方引用着。
2. 想保留旧关系,却用了 set([...])
set 会把不在新列表里的旧关系全部清掉。很多“关系突然少了一半”的问题都来自这里。
3. 在循环里一条条 write 关系字段
如果你已经知道最终目标关系是什么,通常直接构造命令列表一次性写入更稳,也更省 SQL。
读源码时要抓住的一个现实
Command 只是 Python 层的“好名字”。
真正重要的是:ORM 在把命令翻译成关系变化时,会根据字段类型、约束、权限和联动逻辑做不同处理。
这也解释了为什么同样是“加一条关联”,有时候会触发创建子记录,有时候只会多一个中间表行,有时候还会连带影响复制、缓存或 recompute。
实战建议
如果你在写自定义模块,可以按这个顺序判断:
- 你想改的是“记录本身”还是“和当前对象的关系”
- 如果是关系,先想清楚是新增、替换、解绑还是删除
- 能用
Command.*就尽量不用裸三元组 - 遇到异常时,先看最终命令列表,再看 ORM 是否把它转成了你预期的动作
只要把这层协议想明白,x2many 就不会再像魔法。
结尾一句话
One2many / Many2many 的本质不是列表,而是关系变更指令集。
看懂 create / update / delete / unlink / link / clear / set,你就看懂了大半个 Odoo 关系字段写入协议。
DISCUSSION
评论区