计算字段导致无限重新计算(Odoo)

发布: (2026年1月9日 GMT+8 13:00)
5 min read
原文: Dev.to

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 即使在大规模使用下也能保持快速、稳定和可预测。

Back to Blog

相关文章

阅读更多 »

SQLite 中缓存的效率

SQLite 中 Cache 的效率!封面图片用于 “SQLite 中 Cache 的效率” https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=aut...