第四章 ORM 核心操作

篇别:第一篇 基础知识

本章学习目标

  • 熟练使用 CRUD、search、browse 与记录集运算。
  • 掌握 env、sudo、context 的正确用法与风险。
  • 了解 Odoo 19 废弃属性search_fetch / _search_display_name 等新实践。

4.1 CRUD 操作

4.1.1 知识要点

  • create:可传字典列表批量创建。
  • write:批量更新;避免循环内逐条 write。
  • unlink:注意 ondelete、mail 线程、约束。
  • copydefault 覆盖字段。

4.1.2 案例

案例 A:批量创建

self.env['library.book'].create([
    {'name': 'Odoo 开发', 'isbn': '001'},
    {'name': 'Python 基础', 'isbn': '002'},
])

案例 B:安全删除前检查

def unlink(self):
    if any(r.state == 'done' for r in self):
        raise UserError('已完成记录不可删除')
    return super().unlink()

4.1.3 截图占位

图 4-1 日志或 profiler 中一次批量 create 的 SQL 条数对比

4.1.4 本节练习

  1. 简答:为何推荐 create([{...},{...}]) 而非两次 create({...})
  2. 实操:写方法将所选记录 state 批量改为 done(单条 SQL 可用 write 一次完成)。

参考答案提示:1. 减少往返与事务开销。


4.2 查询操作

4.2.1 知识要点

searchreadbrowsedefault_getfields_get;显示名相关向 _search_display_name 演进。

4.2.2 案例

Book = self.env['library.book']
books = Book.search([('name', 'ilike', 'odoo')], limit=10, order='name')
data = books.read(['name', 'isbn'])

4.2.3 截图占位

图 4-2 Odoo Shell 中执行 search 与 read 的交互截图

4.2.4 本节练习

  1. 填空browse 返回的是 __,即使 id 不存在也可能不立即报错(访问字段时校验)。
  2. 实操:用 search_read 取前 20 本书的 nameisbn

参考答案提示:1. 记录集。


4.3 记录集(Recordset)

4.3.1 知识要点

记录集 不可变filteredmappedsortedgrouped(版本 API 以文档为准)。

4.3.2 案例

draft = orders.filtered(lambda r: r.state == 'draft')
names = partners.mapped('name')

4.3.3 截图占位

图 4-3 Python 调试器中查看 recordset 的 _ids 与 env

4.3.4 本节练习

  1. 简答mapped 与 Python 列表推导在 ORM 缓存上的差异概要?

4.4 环境(Environment)

4.4.1 知识要点

env.crenv.userenv.companyenv.contextsudo() 绕过权限,慎用;with_company 多公司。

4.4.2 案例

# 仅演示:在可控边界内为报表汇总切换公司
lines.with_company(company).read(['balance'])

4.4.3 截图占位

图 4-4b 调试器中查看 env.user、env.company 与 context

4.4.4 本节练习

  1. 判断sudo() 后无需再检查业务规则。( )
  2. 实操:用普通用户与超级用户分别 search 同一模型,对比条数差异并解释记录规则影响。

参考答案提示:1. 错。


4.5 高效查询(补充)

4.5.1 知识要点

search_fetchfetch:减少 SQL 次数,缓解 N+1。

4.5.2 案例

records = Model.search_fetch(domain, ['name', 'partner_id'])

具体签名以 19.0 文档为准。

4.5.3 截图占位

图 4-4 ORM 日志:使用 fetch 前后 SQL 查询次数对比

4.5.4 本节练习

  1. 实操:找一段循环内访问 line.product_id.name 的代码,改为预取优化。

4.6 Odoo 19 废弃项(新增)

4.6.1 知识要点

弃用 record._cr_context_uid,统一 record.env.cr

4.6.2 案例

# 旧(避免)
# cr = self._cr

# 新
cr = self.env.cr

4.6.3 截图占位

图 4-5 upgrade_code 或 ruff 对废弃属性的提示

4.6.4 本节练习

  1. 实操:在自定义模块中全局搜索 _cr 并改为 env.cr

4.7 方法装饰器(新增)

4.7.1 知识要点

@api.model@api.model_create_multi@api.depends@api.onchange@api.constrains@api.private(RPC 可见性策略以文档为准)。

4.7.2 案例

@api.model_create_multi
def create(self, vals_list):
    for vals in vals_list:
        vals.setdefault('name', '未命名')
    return super().create(vals_list)

4.7.3 截图占位

图 4-7 官方文档「api 装饰器」相关章节

4.7.4 本节练习

  1. 简答@api.model_create_multi 与重写 create 时忘记调用 super 的风险?

4.8 _search_display_name(新增)

4.8.1 知识要点

自定义「按显示名搜索」时实现 _search_display_name(签名以官方为准),避免低效 name_search 扩展遗留。

4.8.2 案例

伪代码:在模型上重写 _search_display_name,对 codenameilike 组合 domain。

4.8.3 截图占位

图 4-6 Many2one 下拉中输入关键词时的网络请求与 domain

4.8.4 本节练习

  1. 拓展:阅读官方示例,实现按 ISBN 搜书。

本章综合练习

  1. 实现:merge_partners 方法,合并重复 res.partner 并重写子表外键(慎用,先备份)。
  2. 论述:sudowith_user 的区别与审计建议。

本章对应白皮书目录:第四章 ORM 核心操作。