Odoo 开发

Odoo 的 _validate_fields 到底在什么时候跑:create、write 与约束触发窗口讲透

很多人知道 Odoo 有 Python constraints,却说不清它们到底是在 create 前、写库后、inverse 前还是 compute 后触发。本文结合 models.py 源码,拆开 _validate_fields 在 create / write / stored compute 链路中的真实时机,帮你判断校验到底该放哪一层。

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

很多开发者在 Odoo 里写约束时,脑子里其实只有一个模糊印象:

  • SQL 约束在数据库层拦
  • @api.constrains 在 Python 层拦

但真正写复杂模型时,决定成败的往往不是“写哪一种约束”,而是:

它到底在 create / write 的哪个时刻触发?

这个问题如果想不清,很容易出现三种典型事故:

  • 约束跑得太早,依赖字段还没到位
  • 约束跑得太晚,错误已经穿透到后续副作用
  • 非 stored / inversed / computed 字段的边界被混成一团

/home/ubuntu/odoo-temp/odoo/orm/models.py 的实现看,_validate_fields() 不是一个“统一在最后补跑一下”的钩子,而是穿插在不同写入阶段的多段校验点。

一、_validate_fields() 本身到底做什么

源码里这个方法本身其实很克制:

  • 只遍历模型上登记好的 constraint methods
  • 只有当约束声明的字段与本次 field_names 有交集时才触发
  • 如果某些字段在 excluded_names 里,就先跳过
  • 并且会在 sudo() 的 recordset 上执行

也就是说,_validate_fields() 的职责不是“替你决定什么时候校验”,而是:

给当前这一批字段变化,挑出该跑的 Python constraints 并执行。

真正重要的是:是谁在什么时机调用它。

二、write() 不是一次校验,而是“两段校验 + inverse 夹层”

write() 的主链路,顺序大致是:

  1. 检查模型与字段访问权限
  2. write_sequence 写字段
  3. modified(...) 标记依赖重算
  4. 如果涉及层级关系,必要时处理 parent_path
  5. 先校验非 inverse 字段
  6. 再执行 inverse
  7. 再校验 inverse 字段
  8. 最后再做 _check_company 等额外检查

这里最关键的源码就是两次调用:

  • real_recs._validate_fields(vals, inverse_fields)
  • real_recs._validate_fields(inverse_fields)

这说明 Odoo 对 write() 的理解很明确:

第一段:先校验普通写入字段

当字段值已经写进缓存 / 数据层,但 inverse 逻辑还没补齐时,先检查那些不依赖 inverse 完整结果的约束。

第二段:inverse 真正跑完后,再校验 inverse 相关字段

因为 inverse 字段的最终一致状态,往往要等 determine_inverse(...) 完成才成立。

这也是为什么有些约束你写在 @api.constrains 里,感觉“第一次 write 时字段明明有值,却像没准备好”:

  • 不是约束失效
  • 而是你依赖的那个值,也许属于 inverse 阶段才真正稳定的那部分

三、create() 也不是单段完成,而是 stored / inversed 分层进入

create() 的链路同样不是“组装 vals → insert → 最后一次性校验”。

源码里关键步骤是:

  1. _prepare_create_values(...) 补默认值、清理非法字段、处理 precompute
  2. 按 stored / inversed / inherited / protected 分类字段
  3. 如果有 _inherits,先准备或创建父记录
  4. 进入 _create(data_list),先落 stored 字段
  5. 对需要 inverse 的字段执行 determine_inverse(...)
  6. 对每条记录执行: data['record']._validate_fields(data['inversed'], data['stored'])

更关键的是,在 _create() 内部,Odoo 还会对 stored 字段完成一轮:

  • records.modified(self._fields, create=True)
  • 必要时处理其他字段创建
  • 然后:records._validate_fields(name for data in data_list for name in data['stored'])

也就是说,create() 至少有两层校验语义:

1)stored 字段落库后的校验

这是“主记录已经建出来,stored 数据已经有了”的那一层。

2)inversed 字段完成反写后的校验

这是“有些值不是 insert 那一刻就最终稳定,而要靠 inverse 补全”的那一层。

所以 create 阶段写约束时,最容易错判的不是“它会不会触发”,而是“它触发时你依赖的字段是不是已经在正确阶段里完成了”。

四、stored compute 也会在计算完成后再补一次约束

很多人会漏掉第三个入口:stored compute 字段重算完成后也可能触发 _validate_fields()

_compute_field_value() 里,源码明确写了:

  • 如果这是 stored compute 字段
  • 并且当前记录是真实数据库记录
  • 那么会对该计算字段所在的计算组字段再跑一轮 _validate_fields()

这意味着什么?

意味着你的约束不只可能在 create / write 主流程里触发,还可能在:

  • 依赖字段改动
  • 框架安排 stored compute 重算
  • 重算结果回写后

再被补跑一次。

这也是一些约束“看起来不是我直接写那个字段,却也被触发”的根本原因。

五、为什么这会直接影响你该把校验写在哪

1)如果你需要数据库永远不允许脏数据进入

优先考虑 SQL constraints。

因为 Python constraints 的触发点再多,本质仍然是 ORM 生命周期中的应用层校验。

2)如果你需要依赖 ORM 语义、跨字段逻辑、业务可读报错

@api.constrains,但要想清它依赖的字段是在:

  • stored 阶段就齐了
  • inverse 后才齐
  • 还是 compute 后才齐

3)如果你的逻辑本质上是“写入前拒绝某种输入格式”

有时更合适的是:

  • 字段 converter
  • create() / write() 显式前置判断
  • inverse 方法内部自校验

而不是把所有东西都塞进 @api.constrains

六、一个实战判断法:先问“最终值什么时候稳定”

写约束前,先问自己这三个问题:

  1. 这个字段在 vals 里直接就有最终值吗?
  2. 还是要等 inverse 之后才稳定?
  3. 还是它来自 stored compute,需要重算后才完整?

如果第三个问题才是答案,你把约束想成“create / write 当场立即判断”往往就会出错。

结语

_validate_fields() 最值得理解的不是函数体本身,而是它在 Odoo 里被安插的位置。

从源码看,它至少服务于三类窗口:

  • stored 写入后的约束检查
  • inverse 完成后的约束检查
  • stored compute 回写后的约束检查

所以约束设计的核心从来不是“会不会触发”,而是:

当它触发时,你依赖的业务状态是否已经真的稳定。

把这个时间轴想清楚,Odoo 的约束设计就会稳很多。

DISCUSSION

评论区

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