先说结论
在 Odoo 里,@api.model_create_multi 不是一个“可加可不加”的装饰器细节。
它真正解决的是三件事:
- 统一
create()的输入语义:不管外部传单个 dict,还是传list[dict],你的重写层都能按“批量创建”来思考。 - 避免你把
create()写成只适合单条记录的逻辑。 - 让共享计算只做一次,而不是每条记录重复做一遍。
所以它当然和性能有关,但更核心的是:
它在帮你把
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 一条数据”,而是一整套批量管线:
- 检查 create 权限
- 检查用户提供字段的访问权
- 准备默认值与标准化输入
- 对字段进行分类 - stored - inversed - inherited - protected / precomputed
- 最后再组织真正的落库与后续计算
这意味着:
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
评论区