SQL 与 ORM 边界

Odoo 存储计算字段在手写 SQL 后为什么必须显式 modified()

手写 SQL 只改了数据库,不会自动走 Odoo 的缓存失效和依赖传播。要让存储计算字段恢复一致,关键是理解 invalidate_recordset()、modified() 与 flush_recordset() 的分工。

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

很多人第一次绕过 ORM 写 SQL,都会得到一个很迷惑的结果:

  • 数据库里已经改了;
  • Python 里好像也改了;
  • 但某个 stored compute 字段还是旧值;
  • 再跑一段逻辑,结果又像“突然自己好了”。

这不是 Odoo 反复无常,而是你绕开了它最重要的一层:缓存失效 + 依赖传播

/home/ubuntu/odoo-temp/odoo/orm/models.pyodoo/orm/fields.py 看,Odoo 的一致性不是靠“每次写完立刻把所有结果算到位”,而是靠一条调度管线:

  1. 先标记哪些字段变了;
  2. 再找到谁依赖它们;
  3. 把受影响的 stored compute 字段放进待重算集合;
  4. 在合适的时机 recompute / flush。

所以,手写 SQL 之后只改数据库是不够的。你还得把 ORM 的那条管线补回来。

一、ORM 写入时,系统会自动做你手写 SQL 不会做的事

当你通过 ORM 执行 write() 时,modified() 会沿着 trigger tree 往后找:

  • 哪些字段依赖当前字段;
  • 哪些字段是 stored compute;
  • 哪些记录需要进入 tocompute

源码里的注释把这个过程讲得很清楚:字段依赖不是“立刻执行”,而是“建立触发关系”。

换句话说:

ORM 的写入会顺手维护后续重算所需的元数据; 纯 SQL 只会改表,不会改这份元数据。

这就是两者最大的差别。

二、手写 SQL 后,最少要补三步

如果你真的需要原生 SQL,通常要把这三件事补上:

1)让数据库和缓存先别打架

self.env.cr.execute(
    "UPDATE sale_order SET state = %s WHERE id IN %s",
    ['sale', tuple(order_ids)],
)
orders = self.env['sale.order'].browse(order_ids)
orders.invalidate_recordset(['state'])

invalidate_recordset() 做的是缓存清理。没有它,当前环境可能还拿着旧缓存,下一次读到的就不是数据库最新值。

2)告诉 ORM:哪些源字段变了

orders.modified(['state'])

这里的重点不是“把 state 重新写一遍”,而是告诉 ORM:

  • 这个字段刚刚被改过;
  • 它的依赖链要重新传播;
  • 相关 stored compute 字段要进待重算队列。

3)在需要稳定结果前,再触发 flush / recompute

如果你后面马上要读依赖字段,或者要把结果交给别的逻辑,最好显式 flush:

orders.flush_recordset(['amount_total'])

flush_recordset() 会先 _recompute_recordset(),再把值刷向数据库。也就是说,它不是单纯“把缓存写回去”,而是会先把待重算链路走完。

三、为什么 modified() 这么关键

modified() 的源码注释里有一个很重要的概念:trigger tree

它不是“我只知道一个字段变了”,而是会继续向后找:

  • 直接依赖它的字段;
  • 依赖这些字段的字段;
  • 再往后的链条。

所以如果你改的是基础字段,但结果字段没有跟着更新,通常不是 compute 写错了,而是你没有触发这条 tree。

也正因如此,Odoo 在 compute_value() 里会先把字段从 tocompute 里移走,再执行 compute;如果失败,还会放回去。这样做的目的,是避免重算链路里出现“自己触发自己”的死循环。

四、最容易踩的三个坑

1)以为 depends 能管手写 SQL

不能。

@api.depends 只对 ORM 感知到的修改有用。你绕过 ORM 后,依赖树不会自动跑。

2)以为 sudo() 能解决一致性问题

不能。

sudo() 解决的是权限,不是缓存和依赖传播。

3)在 compute 里做副作用

也不推荐。

compute 的语义是“根据依赖算值”,不是“顺手发消息、建单据、调外部接口”。如果你把副作用塞进去,重算时机一变化,副作用就可能重复、漏跑,或者跑在你没想到的事务阶段。

五、实战判断顺序

以后遇到“SQL 改了但字段没变”的问题,可以直接按这个顺序排:

  1. 你是不是绕过 ORM 了?
  2. 有没有 invalidate_recordset()
  3. 有没有 modified()
  4. 依赖字段是不是 stored compute?
  5. 你是不是在读结果前就需要 flush_recordset()

只要顺序对,很多“看起来像 Odoo 抽风”的问题,其实都只是你还没把 ORM 的那条一致性管线补完整。

结论

手写 SQL 不是不能用,但它的代价是:你要自己承担 ORM 本来会自动完成的那部分工作

对于 Odoo 来说,最重要的不是“数据库里有没有改”,而是“缓存、依赖、重算、落库是不是还在同一条链上”。

DISCUSSION

评论区

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