先纠正一个很常见的旧印象
很多人一提到 company_dependent=True,脑子里马上冒出来的还是:
- 这类值不在业务表里
- 系统主要靠
ir.property存 - 读值时再从属性表里按公司找
但如果你对着现在这份 /home/ubuntu/odoo-temp 里的源码看,会发现主逻辑已经不是这套直觉了。
现在的重点更像是:
- 字段本体就是模型上的字段
- 数据库列会变成
jsonb - 不同公司的值按公司 id 存在 JSON 结构里
- 如果当前公司没有专属值,再去走
ir.default回退
所以它不该再被理解成“属性表外挂字段”,而应该理解成:
同一个字段,带着多公司维度一起存。
第一层:company_dependent 为什么会让列类型变成 jsonb
在 odoo/orm/fields.py 里,column_type 这段逻辑很直接:
- 普通字段走自己的
_column_type - 但只要
self.company_dependent为真,返回的就是('jsonb', 'jsonb')
这意味着数据库层不是给每个公司多开一列,也不是单独切表。 它是把“公司 → 值”的映射塞进一个 JSONB 列里。
这样设计有几个现实好处:
- 字段定义仍然统一:开发者写的还是一个字段,不需要为每个公司单独建模。
- 查询层还能继续走 ORM:不是把值扔到完全独立的配置系统里。
- 同字段的多公司值天然聚合:对框架来说更容易表达“当前公司值 + 回退值”。
所以从数据模型角度看,company_dependent 不是“变成另外一种字段系统”,而是:
这个字段仍然是字段,只是它的存储列不再是标量,而是带公司维度的 JSONB。
第二层:读值时不是“随便取一段 JSON”,而是先取当前公司键
同一个文件里的 to_sql() 更能说明问题。
源码会在 SQL 层做一个类似这样的思路:
- 先从 JSONB 列里取当前
env.company.id对应的值 - 如果没有,再把 fallback 值补进来
- 布尔、整数、浮点这类字段还会再做类型转换
也就是说,Odoo 不是把整坨 JSON 交给上层自己解释。 它在字段转 SQL 的时候,就已经把“当前公司视角”编码进去了。
这点特别重要,因为它解释了两件事:
1)为什么搜索、排序、分组还能继续工作
如果 company-dependent 值只是 Python 层拼装,很多 SQL 能力都会变弱。 但现在它在 SQL 生成阶段就能拿到“当前公司下的有效值”,于是:
- domain 搜索还能成立
- 某些排序仍然能落到 SQL
- read_group 也还有机会基于这个有效值继续算
2)为什么切换当前公司后,同一字段看起来像“瞬间换了一个值”
因为你并不是在看一列固定标量。 你看到的是:
- 这列 JSONB 中当前公司键对应的值
- 或者这个公司没有专属值时的 fallback
换公司,本质上就是换读取视角。
第三层:fallback 不是玄学,是 ir.default
fields.py 里的 get_company_dependent_fallback() 也很关键。
它会:
- 取
env['ir.default'] - 切到超级用户
- 切到当前公司
- 调
_get_model_defaults(records._name) - 再从返回字典里拿当前字段名对应的默认值
这说明当某公司没有专属值时,默认回退并不是“读一条神秘属性记录”,而是:
回到这个模型在当前公司语义下的默认值系统。
再看 odoo/addons/base/models/ir_default.py,_get_model_defaults() 会按:
- model
- user
- company
- condition
去找可用默认值,并按优先级选中每个字段最高优先的一条。
所以 company-dependent 字段的回退,不是“没有值就空着”,而是“没有公司专属值时,继续走默认值机制”。
这套设计到底解决了什么问题
它解决的不是“怎么把一个值存下来”这么简单。 真正解决的是:
问题 1:同一业务对象,不同公司需要不同配置
例如同一个 partner、同一个产品、同一个模板,在不同公司下需要看到不同默认字段值。
问题 2:但又不希望为了这个需求把模型拆裂
如果每家公司都复制一份对象,主数据维护会非常痛苦。
问题 3:还得保留“没配专属值时有默认行为”
这就是 JSONB 专属值 + ir.default 回退能同时成立的原因。
它把问题分成两层:
- 有专属值时:按当前公司读 JSONB 对应键
- 没专属值时:退到默认值系统
这个模型比“全有或全无”要柔和得多,也更适合真实业务。
新手最容易误解的 4 件事
误解 1:company_dependent 还是老式 ir.property
就当前源码看,核心存储重点已经是字段列本身的 JSONB 化,而不是把值主存放在另一张属性表里。
误解 2:切公司只是前端显示切换
不是。SQL 生成本身就会按当前公司取值,所以这是一层 ORM/查询语义,不只是界面渲染。
误解 3:没配置当前公司值就一定得到 False
不一定。它还会尝试走 ir.default 的模型默认值回退。
误解 4:它和普通字段一样容易直接写 SQL 排查
因为列是 JSONB,你手写 SQL 时如果还按普通标量字段理解,就很容易看错。 你必须先想清楚:
- 当前公司是谁
- 这个公司键有没有值
- 没值时 fallback 是什么
实战开发时最该注意什么
1)别把“字段在表里”理解成“值就是普通列值”
字段确实在表里,但表里的内容可能是多公司 JSON 结构,不是单值。
2)查异常时,要区分“专属值缺失”还是“fallback 生效”
很多“怎么切公司后数据不对”的问题,根本不是写错了,而是某公司其实没配专属值,系统退回了默认值。
3)自己写 SQL/报表时,要先确认读取口径
ORM 已经帮你做了公司键选择和类型转换;你一旦绕开 ORM,就得自己承担这层复杂度。
4)做多公司配置时,要知道它更像“按公司覆盖”,不是“每公司独立记录”
这能帮你更准确地设计字段语义,也避免把 company-dependent 滥用成“大号配置中心”。
一句话总结
现在的 Odoo 里,company_dependent 更应该这样理解:
字段值主存于模型字段自己的 JSONB 列,读取时按当前公司取键;如果当前公司没有专属值,再回退到
ir.default提供的默认值。
把这条主线看清后,你就不会再把它误判成“神秘属性系统”,而会把它当成一套非常明确的多公司字段存储与回退机制。
DISCUSSION
评论区