beego ORM 源码(下):SQL 生成与方言

1. 参与者

|-----------------------|-----------------|---------------------------------------|
| 结构 | 文件 | 职责 |
| querySet | orm_queryset.go | 链式状态(cond/limit/orders...),值拷贝不可变 |
| Condition | orm_conds.go | 条件树(params \[\]condValue) |
| dbTables | db_tables.go | JOIN 图:表别名 T0..Tn、ON 生成、SQL 片段 |
| dbBase | db.go | 方言无关主干:Read/ReadBatch/Insert/... |
| db_mysql/postgres/... | db_*.go | dbBaser 方言实现 |

2. Condition:条件树(orm_conds.go)

Go 复制代码
type Condition struct { params []condValue }
type condValue struct {
    exprs []string        // ["profile","age"]
    args  []interface{}
    cond  *Condition      // 嵌套子条件(渲染时加括号)
    isOr / isNot / isCond / isRaw bool
    sql   string          // Raw 片段
}

Filter("age__gt",18)cond.And("age__gt", 18);AndCond(c) 会 clone 子树。And/Or 不 clone 自身,链上重复调用同一 seter 时注意共享(值拷贝的 querySet 会浅拷贝 cond 指针,Filter 才深拷贝)。

3. WHERE 渲染:getCondSQL(db_tables.go:333-401)

Go 复制代码
遍历 cond.params:
  首项后用 "AND "/"OR " 连接;isNot → "NOT "
  isCond → 递归 getCondSQL,括号包裹
  isRaw → 直接拼 sql
  普通项:
    exprs 按 "__" split;末段若是 operator(in/between/gt/...完整清单 db.go:38-65)
      → 摘出 operator,前面是字段路径
    parseExprs(218-330):沿 Bootstrap 关系图走 exprs
      遇 rel/reverse 字段 → joinTables 注册 JOIN(T1、T2...),index 更新
      返回 (别名 index, fi)
    GenerateOperatorSQL(db.go:1419-1476):
      gt → "Tn.col > ?";in → "Tn.col IN (?,?)"
      between → "BETWEEN ? AND ?";contains → LIKE '%..%'(转义 \%)、
      exact+nil → "IS NULL";isnull → 按 bool
    拼出 "Tn.col <op> ?" + args

跨表 Filter 的本质:表达式路径上的每个关系字段都会注册进 JOIN 图,WHERE 与 JOIN 由同一份 dbTables 状态生成,保证一致。

4. JOIN 引擎:dbTables

4.1 结构(db_tables.go:495-501 创建)

newDbTables(mi, d.ins):以主模型为 T0;parseRelated(110-168) 把 RelatedSel 字段按 relDepth 展开;每张表 dbTable{mi, index, skip ...}

4.2 getJoinSQL(171-215)

对每张 join 表:

Go 复制代码
ON 条件由字段类型决定:
  fk:     T0.user_id = T1.id
  reverse(one/many): T0.id = T1.user_id
  m2m:    经中间表 T2 双 JOIN
INNER JOIN;字段 Null=true 时 LEFT OUTER JOIN

4.3 其余片段

  • getGroupSQL(404-425)/getOrderSQL(428-460,order_clause 的 Order 结构:- 前缀降序、__.)
  • getLimitSQL(463-483):limit==0 → DefaultRowsLimit;封顶方言 MaxLimit(mysql 无、sqlite 有)
  • getIndexSql(486-492):hints 的 UseIndex 系列转发方言 GenerateSpecifyIndex

5. 主干:readSQL 与 ReadBatch

5.1 ReadBatch(db.go:1095-1290)

Go 复制代码
容器识别:slice→many;struct→one;未注册模型自动 RegisterModel
确定列集:全部 DBcols + 相关 join 表列
newDbTables → parseRelated
readBatchSQL(1292-1381):
  getCondSQL/getGroupSQL/getOrderSQL/getLimitSQL/getJoinSQL/getIndexSql
  组装:SELECT [DISTINCT] cols FROM t T0 [idx] [joins] [where] [group] [order] [limit] [FOR UPDATE]
ReplaceMarks:方言占位符(? → $1..,postgres)
QueryContext → 逐行 Scan
setColsValues(1484+):
  convertValueFromDB(1506-1667):DB 值→Go 类型的宽松转换
    字符串→int/bool/time 按长度 19/10/8 分别解析 datetime/date/time
  setFieldValue:指针/Null*/Fielder 各自处理
  join 列按别名顺序回填关联对象指针,并回设反向引用(1208-1248)

5.2 Count(1384-1416)

aggregate="COUNT(*)" 复用 readSQL;有 GROUP BY 时外包一层 SELECT COUNT(*) FROM (...) AS T

5.3 UpdateBatch / DeleteBatch(842-983 / 1014-1092)

  • UpdateBatch:支持 JOIN 条件;不支持 join update 的方言(sqlite)生成 UPDATE ... WHERE pk IN (SELECT T0.pk FROM ...) 子查询
  • DeleteBatch:先 SELECT 命中 PK,再 DELETE WHERE pk IN (...),最后 deleteRels 级联(db.go:987-1011 按 on_delete)
  • 列表达式:buildSetSQL(932-983)支持 Col_Add/Minus/Multiply/Except 与位运算

6. 方言层:dbBaser(types.go:624-663)

接口方法(节选):SupportUpdateJoin/OperatorSQL/TableQuote/ReplaceMarks/MaxLimit/HasReturningID/TimeToDB/TimeFromDB/GenerateSpecifyIndex...

|----------|-------------------------|------------------------------------|--------------|-----------------------------------|
| 差异点 | mysql | postgres | sqlite | oracle |
| 占位符 | ? | $n(ReplaceMarks) | ? | :n |
| upsert | ON DUPLICATE KEY UPDATE | ON CONFLICT DO UPDATE(≥9.5,必须给冲突列) | ON CONFLICT | MERGE |
| 自增取回 | LastInsertId | LastInsertId | LastInsertId | HasReturningID(INSERT..RETURNING) |
| MaxLimit | 无(可省 LIMIT) | - | LIMIT 必须 | ROWNUM 包装 |

InsertOrUpdate(db.go:528-675)按 alias.Driver 分派------同一段业务语义,四套 SQL

7. 事务与装饰器

7.1 事务(orm.go:529-602)

Go 复制代码
Begin → o.db.(txer).BeginTx → txOrm{ormBase{db: &TxDB{tx}}}
DoTx(doTxTemplate):
    Begin
    panicked := true
    defer: panicked || err!=nil → Rollback else Commit
    task(ctx, txOrm)
    panicked = false

TxDB.RollbackUnlessCommit(239-245):忽略 sql.ErrTxDone 的 Rollback------幂等兜底。

7.2 FilterOrmDecorator(filter_orm_decorator.go)

AddGlobalFilterChain 注册的链在 newDBWithAlias 时生效(只影响之后创建的 Ormer):Invocation{Method/Md/Args/mi/InsideTx/TxName},倒序折叠洋葱;GetTableName/GetPkFieldName 暴露给过滤器做分表路由。内置:bean(事务命名)、otel、opentracing、prometheus。

8. 性能画像

  • 反射成本被元数据摊薄;每行 Scan 仍走 interface{} 切片 + convertValueFromDB,超大数据量用 Values/RowsToStruct 减一层 setFieldValue
  • stmt 缓存命中时省 Prepare(高并发主收益)
  • QuerySeter 值拷贝链:每次 Filter 复制 ~15 字段结构体,廉价;但复用 seter 基座时 Cond 指针共享是隐蔽坑(S12 §2)
  • JOIN 逐表展开,深度无硬限------深链查询(DBC 不当)会炸 SQL 长度

9. 设计赏析

  1. 单一状态源:dbTables 同时喂 WHERE/JOIN/SELECT 列,杜绝"条件与连接不一致"这类 bug。
  1. 方言收口:主干零 if driver==;新增方言=实现 dbBaser + 注册 dbBasers。
  1. 模板事务:doTxTemplate 的 panicked 标志是 Go 闭包事务的经典写法,值得抄。
  1. 宽松类型转换:DB 字符串→Go 数字/时间的 fallback,兼容脏数据;代价是转换失败静默零值------读 convertValueFromDB 注释体会取舍。
相关推荐
企业数字化笔记1 小时前
固定资产还有借用记录能报废吗?Java前置检查、停止折旧与SQL验收
java·开发语言·sql
livemetee1 小时前
MySQL联合唯一索引(code,deleted)与逻辑删除问题
java·数据库
Doris__HE1 小时前
【元脑服务器NF8480G7-NF8480M7技术规格分享】
运维·服务器·数据库·缓存·性能优化
实心儿儿1 小时前
MySQL — 库的操作
数据库·mysql·oracle
bksczm1 小时前
MySQL基础篇之索引
数据库·sql·mysql
Lethehong1 小时前
让数据库真正“长“在 Kubernetes 里:KES-Operator 正式发布,K8s 环境下的 KES 集群管理有了标准答案
数据库
l1t2 小时前
DeepSeek总结的PostgreSQL 19 发生了什么
数据库·postgresql
Pocker_Spades_A2 小时前
时序数据库选型指南:从大数据视角看Apache IoTDB的工业级优势
数据库
梦想平凡2 小时前
情怀棋牌源代码焕新记录(一):全新国风UI与原版工程梳理
java·服务器·数据库·websocket·网络协议·cocos2d