计算字段导致无限重新计算(Odoo)
Source: Dev.to
Problem Statement
设计不良的计算字段会因错误的依赖声明(@api.depends)触发不必要的重新计算或无限循环,导致性能问题和行为不稳定。
Odoo 中出现此问题的原因
- 计算字段依赖自身
- 计算方法写入其他字段
- 在计算方法中使用
write() - 依赖范围过宽或不正确
store=True使用不当- 父 ↔ 子字段相互依赖
结果
- 表单加载缓慢
- CPU 使用率高
- UI 卡顿
- 生产环境中随机崩溃
第一步 – 确认有问题的计算字段
常见症状
- 表单视图一直加载
- 服务器 CPU 突升
- 日志中出现重复的 “Computing field …” 信息
- 删除该字段后问题消失
启用日志:
--log-level=debug
查找:
Computing field
持续重复。
Source: …
第 2 步 – 识别常见错误模式
错误 1:字段依赖自身
@api.depends('total')
def _compute_total(self):
for rec in self:
rec.total = rec.price * rec.qty
导致无限递归计算。
错误 2:在计算方法中写入其他字段
@api.depends('price', 'qty')
def _compute_amount(self):
for rec in self:
rec.amount = rec.price * rec.qty
rec.discount = rec.amount * 0.1 # BAD
每一次写入都会再次触发计算。
错误 3:在计算方法中使用 write()
@api.depends('line_ids.amount')
def _compute_total(self):
for rec in self:
rec.write({'total': sum(rec.line_ids.mapped('amount'))})
导致递归 + 数据库写入。
第 3 步 – 定义正确且最小的依赖
正确的依赖声明
@api.depends('price', 'qty')
def _compute_total(self):
for rec in self:
rec.total = rec.price * rec.qty
规则
- 仅依赖源字段
- 永不包含自身计算字段
- 避免使用
@api.depends('*')
第4步 – 保持 Compute 方法纯净(无副作用)
正确模式
total = fields.Float(compute='_compute_total', store=True)
@api.depends('price', 'qty')
def _compute_total(self):
for rec in self:
rec.total = rec.price * rec.qty if rec.price and rec.qty else 0.0
- 不进行写入
- 不调用 ORM
- 安全且可预测
第5步 – 将逻辑拆分为多个计算字段
良好设计
amount = fields.Float(compute='_compute_amount', store=True)
discount = fields.Float(compute='_compute_discount', store=True)
@api.depends('price', 'qty')
def _compute_amount(self):
for rec in self:
rec.amount = rec.price * rec.qty
@api.depends('amount')
def _compute_discount(self):
for rec in self:
rec.discount = rec.amount * 0.1
- 没有循环依赖
- 清晰的分离
第6步 – 仅在必要时使用 store=True
错误
total = fields.Float(compute='_compute_total', store=True)
仅在以下情况下才使用 store=True:
- 用于搜索域
- 用于报表 /
group by
正确
total = fields.Float(compute='_compute_total')
当字段仅用于 UI,且不参与搜索或分组时。
第 7 步 – 使用 @api.onchange 进行 UI 逻辑(而非 Compute)
错误
@api.depends('qty')
def _compute_price(self):
for rec in self:
rec.price = rec.qty * 10
正确
@api.onchange('qty')
def _onchange_qty(self):
self.price = self.qty * 10
- 无重新计算
- 仅 UI 行为
第 8 步 – 修复父子依赖循环
错误(隐藏循环)
@api.depends('line_ids.total')
def _compute_total(self):
for rec in self:
rec.total = sum(rec.line_ids.mapped('total'))
如果 line.total 依赖于父记录,就会出现无限循环。
安全版本
@api.depends('line_ids.price', 'line_ids.qty')
def _compute_total(self):
for rec in self:
rec.total = sum(
line.price * line.qty for line in rec.line_ids
)
- 直接依赖
- 无递归
第 9 步 – 使用约束而非计算字段进行验证
错误示例
@api.depends('qty')
def _compute_check(self):
if self.qty < 0:
raise ValidationError("Invalid")
正确示例
@api.constrains('qty')
def _check_qty(self):
for rec in self:
if rec.qty < 0:
raise ValidationError("Quantity cannot be negative")
- 无重新计算
- 正确的验证层
第10步 – 最终安全模板(最佳实践)
total = fields.Float(
compute='_compute_total',
store=True,
)
@api.depends('price', 'qty')
def _compute_total(self):
for rec in self:
rec.total = (rec.price or 0.0) * (rec.qty or 0.0)
- 纯计算
- 正确的依赖
- 生产安全
结论
在 Odoo 开发中,无限重新计算问题几乎总是由不纯的计算字段引起的,这些字段会写入数据、依赖自身,或将业务逻辑与计算逻辑混合。解决办法是严格遵守纪律:使用纯粹的 compute 方法、最小化依赖、避免 ORM 写入,并正确分离 UI 逻辑和验证。当计算字段被视为只读计算时,Odoo 的 ORM 即使在大规模使用下也能保持快速、稳定和可预测。