x2many 命令语义

Many2many / One2many 命令不是玄学:Command.create、link、set 到底会改什么?

Odoo 的 x2many 写法常被记成一串“0 到 6”的魔法数字,但真正难点不是背数字,而是搞清它们在 one2many 和 many2many 上的语义并不完全相同。结合 commands.py 与 fields_relational.py,本文把 create、update、delete、unlink、link、clear、set 的真实效果一次讲透。

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

很多 Odoo 开发者对 x2many 命令的第一印象,都是这张口口相传的口诀表:

  • 0 创建
  • 1 更新
  • 2 删除
  • 3 解绑
  • 4 关联
  • 5 清空
  • 6 替换

这张表当然有用,但它最大的问题是:

它只帮你记住了编号,没帮你理解语义。

而实战里真正决定你会不会踩坑的,恰恰不是数字本身,而是下面这几个问题:

  • 同样是 create,在 many2many 和 one2many 上会不会一样?
  • deleteunlink 到底差在哪?
  • set 是不是等于“先 clear 再一堆 link”?
  • 为什么有时你只是想解除关系,结果把子记录删掉了?

这些问题,在 /home/ubuntu/odoo-temp/odoo/orm/commands.pyodoo/orm/fields_relational.py 里其实写得很清楚。

一、先记住一个核心事实:Command 不是魔法,是标准三元组

commands.py 里已经把规则说透了。

每个 x2many 命令本质上都是一个 3 元组:

  1. 第一位:命令编号
  2. 第二位:目标记录 id,或者 0
  3. 第三位: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 更像“给每个父记录补子行”。

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 的反向字段删除策略在起作用

五、linkclearset:这三条命令影响的是“关系集合”

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.pywrite_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”,而是“它被翻译成了哪条命令”。

如果你的真实意图是“把关系最终变成这几个 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 怎么背”,而是理解这三层语义:

  1. 你是在改记录,还是改关系?
  2. 字段是 many2many,还是 one2many?
  3. one2many 的 inverse/ondelete 会不会把“解除关系”变成“直接删除”?

只要这三件事想清楚,Command.create()link()set() 这些命令就不再是玄学,而会变成非常稳定、非常好推理的一套 ORM 表达语言。

DISCUSSION

评论区

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