很多 Odoo 开发者对 x2many 命令的第一印象,都是这张口口相传的口诀表:
- 0 创建
- 1 更新
- 2 删除
- 3 解绑
- 4 关联
- 5 清空
- 6 替换
这张表当然有用,但它最大的问题是:
它只帮你记住了编号,没帮你理解语义。
而实战里真正决定你会不会踩坑的,恰恰不是数字本身,而是下面这几个问题:
- 同样是
create,在 many2many 和 one2many 上会不会一样? delete和unlink到底差在哪?set是不是等于“先 clear 再一堆 link”?- 为什么有时你只是想解除关系,结果把子记录删掉了?
这些问题,在 /home/ubuntu/odoo-temp/odoo/orm/commands.py 和 odoo/orm/fields_relational.py 里其实写得很清楚。
一、先记住一个核心事实:Command 不是魔法,是标准三元组
commands.py 里已经把规则说透了。
每个 x2many 命令本质上都是一个 3 元组:
- 第一位:命令编号
- 第二位:目标记录 id,或者 0
- 第三位:values、ids,或者 0
对应关系是:
Command.create(values)->(0, 0, values)Command.update(id, values)->(1, id, values)Command.delete(id)->(2, id, 0)Command.unlink(id)->(3, id, 0)Command.link(id)->(4, id, 0)Command.clear()->(5, 0, 0)Command.set(ids)->(6, 0, ids)
所以从源码设计上说,Odoo 一直不鼓励你死记“0 到 6”,而是鼓励你直接写:
from odoo import Command
record.write({
'line_ids': [
Command.create({'name': 'A'}),
Command.update(12, {'qty': 3}),
Command.unlink(18),
]
})
这样代码的可读性会高很多。
二、最容易误解的一点:one2many 和 many2many 不是同一种关系
这件事如果没想清楚,后面所有命令都会越用越乱。
many2many 的本质
many2many 更像“主表和标签表之间有一张中间关系表”。
- 主记录可以连很多目标记录
- 目标记录也可以被很多主记录共享
one2many 的本质
one2many 则是“对方表里有一个 many2one 反向字段指回当前记录”。
- 关系不是靠中间表,而是靠子记录上的外键
- 所以某个子记录通常只属于一个父记录
这两个结构差异,直接决定了同一条命令在两种字段上的后果可能完全不同。
三、Command.create():看起来都叫创建,实际含义并不一样
commands.py 里明确写到:
- 对 many2many,会在 comodel 创建一个新记录,然后把
self里的所有主记录都关联到它; - 对 one2many,会为
self中的每个主记录分别创建对应子记录。
这意味着什么?
在 many2many 上
如果你对多个父记录一起 write:
records.write({'tag_ids': [Command.create({'name': 'VIP'})]})
很可能是创建一个新标签,然后多个父记录共同连上它。
在 one2many 上
如果你对多个父记录一起 write 同一个 Command.create(...),ORM 会按父记录分发,变成“每个父记录各自新增一条子行”。
这就是第一个常见误区:
同样是 create,many2many 更像“新建共享目标”,one2many 更像“给每个父记录补子行”。
四、update、delete、unlink:最容易被混淆的三兄弟
1)Command.update(id, values)
这个最直观:修改目标记录本身。
它不是改关系,而是对那条已存在的 related record 做 write(values)。
所以如果是 many2many,别忘了:
- 你改的是那条共享目标记录本身
- 不是只对当前父记录“私有地改一下”
如果这条目标记录被别的业务也连着,它们看到的内容也会一起变。
2)Command.delete(id)
这个是真删除记录。
commands.py 的注释说得很直白:
- 删除数据库里的 related record
- 同时移除关系
在 many2many 上,这一步还可能因为该记录仍被别的父记录引用而删不掉。
所以 delete 的重点不是“取消关联”,而是:
我要让这条目标记录从数据库里消失。
3)Command.unlink(id)
unlink 只保证“解除关系”,不一定删除目标记录。
但对 one2many,要特别小心源码里的说明:
- 如果 inverse field 是
ondelete='cascade',那解除关系时,这条子记录还是会被删掉; - 否则通常是把 inverse many2one 设为 False,记录保留。
所以很多人嘴上说“我只是 unlink 一下”,结果子行没了,不是 ORM 抽风,而是one2many 的反向字段删除策略在起作用。
五、link、clear、set:这三条命令影响的是“关系集合”
Command.link(id)
它只做一件事:把现有目标记录挂上来。
- 不创建新记录
- 不改原记录字段
- 只增加关系
这在 many2many 上非常自然,在 one2many 上则意味着把某条子记录重新指向当前父记录。
Command.clear()
commands.py 里写得很清楚:
它等价于对当前关系里的每条记录逐个执行
unlink。
所以 clear() 的危险点也继承了 unlink 的边界:
- many2many:通常只是清空关联
- one2many:可能触发级联删除,未必只是“摘掉”
Command.set(ids)
源码注释说明:
它的语义等价于:对被移除的关系执行
unlink,对新增的关系执行link。
因此 set([1, 2, 3]) 不是“盲目覆盖成一个数组”那么简单,它其实是在告诉 ORM:
- 旧集合里不在
[1,2,3]的,解除关系; - 新集合里以前没有的,加上关系;
- 已经存在的,保持不动。
这也是为什么 set() 特别适合“我想表达最终集合状态”,而不是逐个手工比对差异。
六、源码里真正落地这些语义的地方:fields_relational.py
fields_relational.py 的 write_batch() 非常值得看。
它先把各种输入归一化:
- 元组 ->
Command.set(...) - recordset ->
Command.set(record_ids) False/None->Command.clear()- 普通 id 列表 ->
Command.set(tuple(ids))
也就是说,很多你以为自己是在“传列表”,ORM 其实已经帮你翻译成了一条明确命令。
然后 ORM 再根据字段类型去执行真正的 write_real() / write_new()。
这一步意味着一个很重要的结论:
同样的 Python 输入,最终是先被解释成统一命令,再按字段类型落地。
所以问题的关键,从来都不是“我传的是 list 还是 tuple”,而是“它被翻译成了哪条命令”。
七、为什么 set() 经常比一串 link() 更稳
如果你的真实意图是“把关系最终变成这几个 id”,那 set() 往往比你手写多条 link() / unlink() 更清晰。
因为 set() 直接表达的是目标状态,而不是一串操作步骤。
这种写法的好处是:
- 可读性高
- 更不容易漏掉旧关系
- 对 many2many 尤其合适
但 one2many 上要小心:
- 被移除的旧子记录不是“自动安全存放一边”
- 它们会按
unlink逻辑处理 - 如果 inverse 是 cascade,可能直接删库
八、实战里最容易踩的坑
坑 1:把 delete 当成“解除关联”
delete 是删除记录本身,不只是去掉关系。
坑 2:在 many2many 上 update 共享记录,却以为只影响当前父记录
不是的。你改的是同一条目标记录,别的父记录也会看到变化。
坑 3:在 one2many 上 clear() / set(),却没想到会触发子记录删除
如果 inverse many2one 是 ondelete='cascade',这很正常。
坑 4:还在代码里硬写 (0, 0, vals) 这种裸数字
不是不能写,但长期维护时非常不友好。能用 Command.create() 就尽量用。
九、我建议的选择规则
如果你只是想快速判断该用哪条命令,可以用这个最简版思路:
- 我要新增一条相关记录 ->
Command.create() - 我要改现有相关记录内容 ->
Command.update() - 我要彻底删掉相关记录 ->
Command.delete() - 我只想解除关系 ->
Command.unlink() - 我要挂上一个现有记录 ->
Command.link() - 我要清空所有关系 ->
Command.clear() - 我要把最终集合替换成指定 ids ->
Command.set()
然后再补一个边界提醒:
一旦字段是 one2many,就必须额外想一下 inverse 字段和
ondelete策略。
总结
x2many 命令真正难的地方,从来不是“0 到 6 怎么背”,而是理解这三层语义:
- 你是在改记录,还是改关系?
- 字段是 many2many,还是 one2many?
- one2many 的 inverse/ondelete 会不会把“解除关系”变成“直接删除”?
只要这三件事想清楚,Command.create()、link()、set() 这些命令就不再是玄学,而会变成非常稳定、非常好推理的一套 ORM 表达语言。
DISCUSSION
评论区