第六章 交叉引用

篇别:第一篇 基础知识

本章学习目标

  • 准确理解 XML ID(外部标识符) 在数据库中的存储与命名规则。
  • 能在 XML 与 Python 中正确使用 refenv.ref,杜绝硬编码数字 ID。
  • 能规划 跨模块依赖数据文件加载顺序,避免升级期「找不到外部 ID」类错误。
  • 能排查 noupdate、重复 ID、模块卸载 对交叉引用的影响。

导读:为什么需要外部 ID

PostgreSQL 中的主键(数字)在 不同数据库、备份恢复、演示库 之间 不一致。若代码写 browse(7),换库后可能指向完全错误的记录。XML ID 将「逻辑名」module.name 映射到「当前库中的 res_id」,是模块安装与升级时 稳定引用 的唯一推荐方式。


6.1 XML ID 与外部标识符

6.1.1 知识要点

  • 存储模型:ir.model.data(常称「外部标识符」记录)。
  • 完整 ID = 模块名.数据文件中 id 属性,如 sale.action_orders
  • XML 文件里 <record id="action_orders" ...> 在模块 sale 中即 sale.action_orders
  • noupdate="1" 的数据:升级时可能 不再覆盖 该记录字段,但 XML ID 行仍存在ref 仍可用。

6.1.2 ir.model.data 关键字段(理解用)

字段 含义
module 定义该 id 的模块名
name 记录在本模块内的短名(与 XML id 对应)
model 目标模型,如 ir.actions.act_window
res_id 目标表中的整数主键

6.1.3 案例

案例 A:XML 中 ref 赋 Many2one

<record id="library_book_access_user" model="ir.model.access">
  <field name="name">library.book user</field>
  <field name="model_id" ref="model_library_book"/>
  <field name="group_id" ref="base.group_user"/>
  ...
</record>

model_library_book 通常由框架为 library.book 模型自动生成对应 ir.model 数据行的 XML id(具体以模块 base/ir 规则为准);自定义模型安装后会存在可引用 id。

案例 B:Python 中安全解析

def _get_book_action(self):
    action = self.env.ref('my_library.action_book', raise_if_not_found=False)
    if not action:
        return {}
    return action.read()[0]

raise_if_not_found=False 用于可选扩展:某模块未安装时不应崩溃。

案例 C:eval 中组合 ref

<field name="binding_model_id" ref="model_library_book"/>
<field name="groups_id" eval="[(4, ref('base.group_user'))]"/>

6.1.4 截图占位

图 6-1 技术 → 外部标识符:模块、名称、模型、记录 ID

图 6-1b 同一条记录在表单中展开「复制完整外部 ID」

6.1.5 本节练习

  1. 判断:同一数据库中,不同模块可以使用相同的「短 id」action_book,只要完整 XML ID 不同。( )
  2. 实操:在技术菜单中找到任意一条 ir.ui.menu,记下其 完整外部 ID
  3. 简答:为何禁止在生产自定义代码中写 self.env['ir.model.data'].browse(123) 固定引用?

参考答案提示:1. 对(短名在 module 内唯一,完整 id 为 module.name)。3. 数字 id 不稳定且难读。


6.2 跨模块数据引用

6.2.1 知识要点

  • depends__manifest__.py 中必须列出 被引用 XML ID 所属模块,保证 加载顺序
  • 数据文件顺序:同一模块内 data 列表 自上而下 加载;先定义的 id 可被后续文件 ref
  • 不要循环依赖:模块 A depends B 且 B depends A 会导致安装失败。
  • 演示数据:放在 demo 键;生产库 勿依赖 demo 中的 id 作为业务前提。

6.2.2 案例

案例 A:依赖销售并在其菜单下挂接

# __manifest__.py
'depends': ['sale', 'base'],
<menuitem id="menu_library_from_sale" name="图书"
          parent="sale.sale_menu_root"
          action="my_library.action_book"
          sequence="99"/>

若漏写 'sale',升级时常见报错:External ID not found: sale.sale_menu_root

案例 B:引用他模块 ir.sequence

模块 my_library 依赖 stock(举例),在 XML 中:

<record id="seq_library_card" model="ir.sequence">
  <field name="name">借书证号</field>
  <field name="prefix">LC</field>
  <field name="padding">6</field>
</record>

另一模块 my_library_card 需引用时:

'depends': ['my_library'],
<field name="sequence_id" ref="my_library.seq_library_card"/>

案例 C:Python 动态拼模块前缀(慎用)

仅在多品牌/多套件下偶尔使用;默认应 字面量 env.ref('my_module.id') 以便静态检查。

6.2.3 截图占位

图 6-2 `__manifest__.py` 中 depends 与 data 列表示意

6.2.4 本节练习

  1. 简答dependsimport 在 Python 文件里 from odoo.addons.sale import ... 有何不同职责?
  2. 实操:故意移除 depends 中的被引用模块,记录完整报错信息到学习笔记。
  3. 情景题:模块 A 的数据文件 ref 了模块 B 的 id,但 A 的 depends 只写了 base,在什么操作时失败(安装 A?安装 B?升级 A?)?

参考答案提示:3. 安装/升级加载 A 的数据时,若 B 未先装则 ref 失败。


6.3 常见错误与调试

6.3.1 知识要点

错误信息关键词 常见原因
External ID not found 模块未装、depends 缺失、拼写错误、id 在 demo 中未加载
ParseError / ref XML 语法或 ref 目标不存在
重复 key 两模块定义了相同 module.name(冲突)

6.3.2 案例:用 Shell 验证

./odoo-bin shell -d mydb
env.ref('my_library.action_book')
env['ir.model.data'].search([('module', '=', 'my_library'), ('name', '=', 'action_book')])

6.3.3 截图占位

图 6-3 odoo shell 中 env.ref 成功与异常对比

6.3.4 本节练习

  1. 实操:用 ir.model.data 搜索 name=action_book 的所有模块,理解「同名短 id 多模块」现象。
  2. 简答:卸载模块是否会删除其创建的 ir.model.data?对残留数据有何影响?

参考答案提示:2. 通常随模块卸载清理相关数据(视模型 _depends 与卸载策略);残留会导致重装冲突,需技术清理。


本章综合练习

  1. 列举 三种 必须使用 env.ref 而不能使用固定整数 id 的场景。
  2. 故障排查:升级后某菜单消失——逐步列出检查项(外部 id 是否存在、菜单 active、用户组、parent 是否被删、视图继承是否报错阻断升级)。
  3. 设计:两个独立模块都要向 同一父菜单 增加子菜单,如何组织 dependssequence 避免顺序随机?
  4. 拓展:阅读官方「Module manifest」文档中 data / demo 的说明,用一句话总结生产环境建议。

本章对应白皮书目录:第六章 交叉引用。