create 批量语义

Odoo 重写 create 为什么最好用 @api.model_create_multi:这不只是“性能优化”

很多人知道 Odoo 里 create 可以批量创建,却没真正理解 @api.model_create_multi 在保护什么。本文结合官方 ORM 源码,讲清它和批量语义、默认值传播、重写姿势之间的真实关系。

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

先说结论

在 Odoo 里,@api.model_create_multi 不是一个“可加可不加”的装饰器细节。

它真正解决的是三件事:

  1. 统一 create() 的输入语义:不管外部传单个 dict,还是传 list[dict],你的重写层都能按“批量创建”来思考。
  2. 避免你把 create() 写成只适合单条记录的逻辑
  3. 让共享计算只做一次,而不是每条记录重复做一遍

所以它当然和性能有关,但更核心的是:

它在帮你把 create() 从“单条表单思维”拉回到 Odoo ORM 的“recordset / batch 思维”。


官方源码到底做了什么

/home/ubuntu/odoo-temp/odoo/orm/decorators.py 里,model_create_multi 的实现很直接:

  • 如果传进来的是单个 dict
  • 就把它包装成 [vals]
  • 然后统一交给你的方法

也就是说,这个装饰器并不会神奇地“自动提速”。

它做的核心工作是:把输入规格统一成 vals_list

而在 /home/ubuntu/odoo-temp/odoo/orm/models.py 里,BaseModel 的 create() 本身就是用 @api.model_create_multi 定义的。官方默认就把创建流程设计成批量入口,而不是一条一条临时拼出来。

这说明 Odoo 官方的心智模型非常明确:

create() 的标准形态,本来就应该是“接一批值,然后一次性组织创建流程”。


为什么很多人会把 create 写坏

最常见的错误是这种:

@api.model
def create(self, vals):
    vals['code'] = self.env['ir.sequence'].next_by_code('x.demo')
    return super().create(vals)

这段代码在“看起来只会从表单单条创建”的场景下,经常一时还能跑。

但问题是,Odoo 的创建入口并不只有:

  • 后台表单保存
  • 还有 import
  • one2many 内联批量新建
  • 测试代码批量造数
  • 系统初始化数据
  • 某些业务链路里的一次性多记录创建

一旦上游传的是 list,这种写法就开始出现问题:

  • 你可能只处理了第一条
  • 你可能把 list 当 dict 改炸
  • 你可能共享逻辑被迫重复执行很多次
  • 你可能引入“有时单条、有时多条”的隐性 bug

所以问题不是“这段代码能不能跑”,而是:

它是不是和 Odoo 官方对 create() 的抽象一致。


BaseModel.create 真正关心什么

从官方 models.py 那段实现看,create() 真正做的不是“直接 insert 一条数据”,而是一整套批量管线:

  1. 检查 create 权限
  2. 检查用户提供字段的访问权
  3. 准备默认值与标准化输入
  4. 对字段进行分类 - stored - inversed - inherited - protected / precomputed
  5. 最后再组织真正的落库与后续计算

这意味着:

  • create() 不是纯 SQL 包装
  • 它是一个很重的 ORM 生命周期入口
  • 它天然适合“先把一批数据整理好,再一起过流程”

所以你在重写时,最应该做的是:顺着这条批量管线插入你的业务规则,而不是把它硬拽回“一次只建一条”。


@api.model_create_multi 最值钱的地方,不是快,而是“共享准备”

举几个非常典型的例子。

场景 1:共享查一次配置

比如你要读某个参数、某个精度、某个配置开关。

错误写法往往是循环里一条条查。

正确思路是:

  • 先拿到 vals_list
  • 共享配置只查一次
  • 再循环补每条记录

场景 2:统一预处理输入

比如你要:

  • 统一补默认公司
  • 统一整理名称
  • 统一修正某些缺省值

这些事情本质上都是“面向整批输入”的,而不是“写完一条再看下一条”。

场景 3:避免副作用顺序失控

如果你把重逻辑写在单条分支里,很容易出现:

  • 第一条创建后已经改了环境状态
  • 第二条在不同上下文里被创建
  • 批量输入反而产生不一致结果

批量入口至少能让你先看全局,再决定怎样处理每条。


新手最容易误解的点:用了它,不代表什么都能“批量安全”

@api.model_create_multi 只是帮你把输入统一成 list。

不会自动保证你的业务逻辑天然支持批量

比如下面这种代码,虽然装饰器写对了,但思路还是单条:

@api.model_create_multi
def create(self, vals_list):
    for vals in vals_list:
        vals['code'] = self.env['ir.sequence'].next_by_code('x.demo')
    return super().create(vals_list)

这段并不一定错,但你至少要知道:

  • sequence 是否允许这样逐条取号
  • 是否存在多公司、上下文、默认值差异
  • 是否某些共享校验本该在循环外统一做

所以真正的标准不是“写了装饰器就毕业”,而是:

你的 create 重写,是否真的以批量语义设计。


什么时候必须优先考虑 model_create_multi

凡是你在重写 create(),而且出现下面任一特征,就应该优先这么写:

  • 可能被 import 批量调用
  • 可能从 one2many 一次创建多行
  • 需要共享读取配置 / 环境数据
  • 需要避免重复查询
  • 需要先全量看输入再决定逻辑

实际开发里,这几乎就是绝大多数 create() 重写场景。

所以更贴近实战的经验其实是:

除非你非常确定这是一个完全单条语义的入口,否则默认就按 @api.model_create_multi 写。


一个更稳的重写模板

@api.model_create_multi
def create(self, vals_list):
    vals_list = [vals.copy() for vals in vals_list]

    company = self.env.company
    setting = self.env['ir.config_parameter'].sudo().get_param('x_demo.enabled')

    for vals in vals_list:
        if setting and not vals.get('company_id'):
            vals['company_id'] = company.id

    records = super().create(vals_list)
    return records

这里的关键点不是模板本身,而是思路:

  • 输入先标准化
  • 共享数据在循环外准备
  • 循环只做逐条差异补充
  • 最后一次性交给 super()

这就是 Odoo 官方 batch ORM 思路在自定义代码里的自然延伸。


和现有文章怎么区分

这篇不是在重复讲 create/write/copy 生命周期分工,也不是重讲 ORM 批处理性能。

本文的切入点更具体:

  • 为什么 create() 的官方抽象本来就是批量入口
  • @api.model_create_multi 在输入规格上到底保护了什么
  • 为什么很多“能跑”的 create 重写,从 batch 语义上其实是错的

也就是从“create 重写的输入契约”来讲问题,而不是泛泛谈生命周期。


最后一句话

在 Odoo 里,create() 从来不是“表单点保存就写一条”的小接口。

它是 ORM 的正式入口。

@api.model_create_multi 的意义,就是提醒你:

别把 Odoo 的批量世界,写回成只适合单条表单的世界。

DISCUSSION

评论区

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