Odoo ORM 将 Python 查询意图编译为 SQL 的逐函数讲解(Odoo 19)

0. 先给结论(超短版)

Odoo ORM 的 SQL 生成主链路是:

search()/search_count()

models._search()

Domain.optimize_full()

Domain._to_sql()(递归)

Field.condition_to_sql()(叶子条件编译)

Query.select()/subselect()

env.execute_query(...)

它不是把 Python 语句逐行翻译,而是把"查询意图(domain AST)"编译成参数化 SQL。


1) 调用栈 A:search_count(domain)

A-1. models.py::search_count()(约 1347)

复制代码
query = self._search(domain, limit=limit)
return len(query)
  • search_count 不自己写 SQL。
  • 它拿到 Query 对象后调用 len(query)

A-2. query.py::__len__()

  • 若 Query 还没 materialize:
    • limit/offset 时,生成 SELECT COUNT(*) FROM (<subquery>) t
    • 否则生成 SELECT COUNT(*)
  • 然后执行 SQL。

关键点:count 也是复用同一 Query 抽象,不是独立 SQL 通道。


2) 调用栈 B:search(domain, offset, limit, order)

B-1. models.py::search()(约 1363)

复制代码
return self.search_fetch(domain, [], ...)
  • searchsearch_fetch,尽量减少 SQL 次数。

B-2. models.py::search_fetch()

复制代码
query = self._search(domain, offset=..., limit=..., order=...)
if query.is_empty(): return self.browse()
return self._fetch_query(query, fields_to_fetch)
  • 第一步仍是 _search()
  • _search 只"构建查询",不一定马上执行。

3) 调用栈核心:models.py::_search()(约 5319)

这是最关键函数,逐段看:

C-1. 权限预检

复制代码
check_access = not (self.env.su or bypass_access)
if check_access:
    self.browse().check_access('read')

C-2. 标准化 Domain + active_test 注入

复制代码
domain = Domain(domain)
if active_test ... and domain 未显式涉及 active 字段:
    domain &= Domain(self._active_name, '=', True)

C-3. 关键优化:optimize_full

复制代码
domain = domain.optimize_full(self)
  • 会做 FULL 级优化(可用模型 search method、继承字段解析等)。
  • 后续 _to_sql() 依赖这个步骤(否则断言失败)。

C-4. 初始化 Query,并注入业务域 SQL

复制代码
query = Query(self.env, self._table, self._table_sql)
if not domain.is_true():
    query.add_where(domain._to_sql(self, self._table, query))

C-5. 注入安全域(record rules)

复制代码
sec_domain = self.env['ir.rule']._compute_domain(self._name, 'read')
sec_domain = sec_domain.optimize_full(self_sudo)
query.add_where(sec_domain._to_sql(...))
  • 这一步非常关键:权限域合并在 SQL WHERE 里

C-6. ORDER/LIMIT/OFFSET 下推到 Query

复制代码
if order: query.order = self._order_to_sql(order, query)
if limit is not None: query.limit = limit
if offset is not None: query.offset = offset

返回 Query。


4) Domain 编译栈:domains.py

D-1. Domain.optimize_full(model)(约 435)

  • self._optimize(model, OptimizationLevel.FULL)
  • _optimize 是固定点迭代:一层层优化直到稳定。

D-2. 各种 _to_sql(model, alias, query)

关键节点:

  • DomainBool._to_sqlTRUE/FALSE
  • DomainNot._to_sql(child) IS NOT TRUE
  • DomainNary._to_sql(And/Or):拼接 AND/OR
  • DomainCondition._to_sql(约 1082):叶子条件编译入口

DomainCondition._to_sql 的核心:

复制代码
field = self._field(model)
model._check_field_access(field, 'read')
return field.condition_to_sql(field_expr, operator, value, model, alias, query)

这句很关键:domain 叶子最终交给 Field 自己去生成 SQL。


5) Field 编译栈:models._field_to_sql + fields.condition_to_sql

E-1. models.py::_field_to_sql(alias, field_expr, query)(约 2910)

职责:把"字段表达式"变成 SQL 表达式。

  • 解析 field_expr(支持 property/path)
  • 处理 related 非存储字段:路径展开 + join
  • field.to_sql(...)
  • 若有 property:field.property_to_sql(...)

所以这是"字段表达式 lowering 层"。

E-2. fields.py::condition_to_sql(约 1244)

复制代码
sql_expr = self._condition_to_sql(...)
if self.company_dependent:
    sql_expr = self._condition_to_sql_company(...)
return sql_expr

E-3. fields.py::_condition_to_sql(约 1257)

按 operator 分发:

  • in/not in(含 null/falsy 语义修正)
  • like/ilike(含 unaccent)
  • >,<,>=,<=
  • any!/not any!(接 Query 或 SQL)

全程使用 SQL("...", params...),参数化而不是直接拼值。


6) 关系字段专线(最容易踩坑)

关系字段并不总走 base Field 逻辑,会被子类覆盖。

F-1. fields_relational.py::Many2one.condition_to_sql(约 467)

当 operator 为 any/not any/any!/not any!field_expr==self.name 时:

  1. 先取 sql_field = model._field_to_sql(alias, field_expr, query)
  2. 决定走 LEFT JOIN 路线还是子查询 IN/NOT IN 路线
  3. 若 value 是 Domain:可能转成 comodel._search(value, ...)
  4. 最终组装:
    • field IN (subselect)field NOT IN (subselect)
    • 或 JOIN 后直接把 comodel domain SQL 并入

洞察:many2one 的 any/not any 并不是固定 SQL 模板,会按可空性、正负条件、访问策略动态选策略。

F-2. _RelationalMulti.condition_to_sql(约 766)

给 one2many/many2many 的通用入口:

  • in/not in 规范成 any/not any
  • 把 value(collection/Domain/SQL/Query)统一转换为 Query
  • _condition_to_sql_relational(...) 由具体字段落 SQL

F-3. One2many._condition_to_sql_relational(约 1158)

主模板是 EXISTS/NOT EXISTS,核心思路:

  • 从 coquery 拿到 inverse 字段
  • 生成 EXISTS (SELECT FROM <subquery> WHERE __inverse = alias.id)
  • 某些非存储 inverse 的场景会退化为先求 ids 再 IN/NOT IN

F-4. Many2many._condition_to_sql_relational(约 1691)

基于关系表(relation/column1/column2)生成:

  • EXISTS (SELECT 1 FROM rel_table ... AND rel_id2 IN (coquery))
  • 若 coquery 没有 where,会走更轻量 existence 判断分支。

7) Query 组装栈:query.py

G-1. Query.__init__

  • 初始化 FROM 主表
  • 维护 _joins_where_clausesgroupby/having/order/limit/offset

G-2. add_join/add_where

  • 渐进式构建 join/where,不立刻执行。

G-3. select(*args)(约 173)

统一输出:

复制代码
SELECT ...
FROM ...
WHERE ...
GROUP BY ...
HAVING ...
ORDER BY ...
LIMIT ...
OFFSET ...

G-4. subselect(*args)(约 188)

  • 生成可嵌套子查询
  • 某些情况下移除不必要 ORDER,提高效率

G-5. get_result_ids / __len__ / __iter__

  • Query 在"被消费"时才执行 SQL(惰性)

8) ORDER BY 子栈:models._order_to_sql

_order_to_sql(约 5219)会:

  • 解析 order 字符串并校验合法性
  • 每个字段交给 _order_field_to_sql
  • many2one 排序时可能递归到 comodel 的 _order_to_sql

这也是为什么看似简单 order='partner_id' 也可能引入 join 与复合排序。


9) read_group 调用栈(聚合)

高层 read_group(约 2749)最终走 _read_group(约 1861):

  1. query = self._search(domain)
  2. _read_group_groupby/_read_group_select 生成 group/select 项
  3. query.order = _read_group_orderby(...)
  4. query.having = _read_group_having(...)
  5. env.execute_query(query.select(*select_args))

本质 :read_group 是在 _search 结果上继续"加 group/having 的 SQL codegen"。


10) 一条真实"函数级"栈图(search 场景)

复制代码
Model.search(domain)
  -> Model.search_fetch(...)
    -> Model._search(domain,...)
      -> Domain(domain)
      -> Domain.optimize_full(model)
        -> Domain._optimize(...)
      -> Query(...)
      -> Domain._to_sql(model, alias, query)
        -> DomainAnd/Or/Not._to_sql(...) [递归]
        -> DomainCondition._to_sql(...)
          -> field.condition_to_sql(...)
            -> field._condition_to_sql(...)
              -> model._field_to_sql(...)
      -> ir.rule._compute_domain(...)
      -> sec_domain._to_sql(...)
      -> model._order_to_sql(...)
      -> return Query
    -> _fetch_query(query,...)
      -> query.select(...)
      -> env.execute_query(...)

11) 为什么这套架构比"直接拼 SQL"强

  1. 安全统一:字段读权限、record rules 与业务域在同一 SQL 编译流程。
  2. 可优化:Domain 可做规则化/简化,不必每个模块自己优化。
  3. 可组合:Query 可嵌套(subselect),domain 可组合(AND/OR/NOT)。
  4. 类型语义集中:各字段类型自己决定 SQL 语义,避免散落在业务代码。

12) 你读源码时的最小抓手(建议顺序)

  1. models.py::_search(主干)
  2. domains.py::DomainCondition._to_sql(域到字段编译入口)
  3. fields.py::_condition_to_sql(操作符细节)
  4. fields_relational.py(m2o/o2m/m2m 的 any/not any)
  5. query.py::select/subselect(最终 SQL 形态)

按这个顺序读,几乎能覆盖 90% ORM->SQL 的真实路径。

相关推荐
那我掉的头发算什么1 小时前
【图书管理系统】基于Spring全家桶的图书管理系统(上)
java·服务器·数据库·spring boot·后端·spring·mybatis
廋到被风吹走1 小时前
SOLID原则深度解析:面向对象设计的五大基石
java·log4j
shalou29011 小时前
MySQL数据库的数据文件保存在哪?MySQL数据存在哪里
数据库·mysql
byte轻骑兵1 小时前
大数据场景时序数据库选型指南——Apache IoTDB实践与解析
大数据·数据库·apache·时序数据库·iotdb
数据与人1 小时前
MySQL int(10) 与 int(11) 的区别
数据库·mysql
cjl_8520082 小时前
MS SQL Server 实战 排查多列之间的值是否重复
java
e***8902 小时前
mysql之如何获知版本
数据库·mysql
海兰2 小时前
ES 9.3.0 日志模式分析
java·大数据·elasticsearch
桂花很香,旭很美2 小时前
[7天实战入门Go语言后端] Day 4:Go 数据层入门——database/sql 与简单 CRUD
数据库·sql·golang