夜雨聆风学习资料网

ARTICLE · 993429

GORM statement.go 源码详解

GORM statement.go 源码详解

文件路径: gorm.io/gorm/statement.go
相关依赖: clauseloggerschemautils 包

Statement 是 GORM 在生成与执行 SQL 过程中最核心的中间状态载体。它既保存了「用户意图」(Model、Dest、Where 等条件)、又保存了「SQL 渲染结果」(SQL Builder 与参数 Vars),是整个 ORM 引擎的数据中枢。

本文件可大致分为四个部分:

  1. 1. 数据类型定义Statement 结构体、joinStatementModifier 接口)
  2. 2. SQL 字符写入与引用WriteString / WriteByte / WriteQuoted / QuoteTo / Quote
  3. 3. 子句与参数管理AddClause / AddClauseIfNotExists / AddVar / Build / BuildCondition
  4. 4. 模型解析与元信息Parse / ParseWithSpecialTableName / clone / SetColumn / Changed / SelectAndOmitColumns

一、数据类型定义

1.1 Statement 结构体

type Statement struct {
    *DB
    TableExpr            *clause.Expr
    Table                string
    Model                interface{}
    Unscoped             bool
    Dest                 interface{}
    ReflectValue         reflect.Value
    Clauses              map[string]clause.Clause
    BuildClauses         []string
    Distinct             bool
    Selects              []string
    Omits                []string
    ColumnMapping        map[string]string
    Joins                []join
    Preloads             map[string][]interface{}
    Settings             sync.Map
    ConnPool             ConnPool
    Schema               *schema.Schema
    Context              context.Context
    RaiseErrorOnNotFound bool
    SkipHooks            bool
    SQL                  strings.Builder
    Vars                 []interface{}
    CurDestIndex         int
    attrs                []interface{}
    assigns              []interface{}
    scopes               []func(*DB) *DB
    Result               *result
}

字段分为五个功能域(详细字段语义请参见同目录下的 statement_struct_fields.md):

功能域
字段
模型与表*DB
(嵌入)、ModelTableTableExprSchemaDestReflectValue
SQL 构建Clauses
BuildClausesSQLVarsDistinctSelectsOmitsJoinsColumnMapping
执行控制ConnPool
ContextUnscopedRaiseErrorOnNotFoundSkipHooksCurDestIndex
关联与数据Preloads
Settingsattrsassignsscopes
执行结果Result

特别注意嵌入的 *DB 字段:它让 Statement 能直接调用 DB 的方法(AddErrorSessionDialectorNamingStrategy 等),是 GORM 链式代理(DB.getInstance() → Statement)得以无缝衔接的关键。

1.2 join 结构体

type join struct {
    Name       string
    Alias      string
    Conds      []interface{}
    On         *clause.Where
    Selects    []string
    Omits      []string
    Expression clause.Expression
    JoinType   clause.JoinType
}

描述一次 JOIN 的完整信息:

  • • Name:被关联的表名或关联名;
  • • Alias:JOIN 表别名;
  • • Conds:连接条件(通常是与主表主键等列的匹配条件);
  • • OnON 子句的表达式;
  • • Selects / Omits:JOIN 时附带选择/忽略的列;
  • • Expression:原始 SQL JOIN 表达式(如 LEFT JOIN xxx ON ...);
  • • JoinTypeLEFT / RIGHT / INNER 等类型。

Joins 字段([]join)以 slice 形式保存多条 JOIN,供构建 FROM 子句时使用。

1.3 StatementModifier 接口

type StatementModifier interface {
    ModifyStatement(*Statement)
}

如果一个 clause.Interface 额外实现了该接口,那么它就不是「往某个子句里合并」,而是直接修改整个 Statement。这是 GORM 为少数特殊子句(如 clause.Lockingclause.For)提供的自治改写入口(在 AddClause 中判断)。


二、SQL 字符写入与引用

2.1 WriteString(str string) (int, error)

func(stmt *Statement) WriteString(str string) (interror) {
return stmt.SQL.WriteString(str)
}

把一段未经处理的字符串直接追加到 stmt.SQLstrings.Builder)中,返回写入字节数与错误。几乎所有 SQL 片段(关键字、表名、=( 等)都经由它落进 SQL Builder。

2.2 WriteByte(c byte) error

func(stmt *Statement) WriteByte(c byteerror {
return stmt.SQL.WriteByte(c)
}

追加单个字节到 SQL Builder,用于写入分隔符、(),、空格等。

2.3 WriteQuoted(value interface{})

func(stmt *Statement) WriteQuoted(value interface{}) {
    stmt.QuoteTo(&stmt.SQL, value)
}

便捷入口:把 value 按引用规则写入当前的stmt.SQL。常用于把表名/列名带引号地写入正在构建的 SQL。

2.4 QuoteTo(writer clause.Writer, field interface{})

func(stmt *Statement) QuoteTo(writer clause.Writer, field interface{}) {
    write := func(raw bool, str string) {
if raw {
            writer.WriteString(str)
        } else {
            stmt.DB.Dialector.QuoteTo(writer, str)
        }
    }
// ... 类型分发
}

这是最核心的「按类型引用写出」方法。闭包 write 决定是原样写出(raw=true)还是交给方言(Dialector)加引号。根据 field 的动态类型分发:

类型
处理逻辑
clause.Table
若表名为 CurrentTable 且存在 TableExpr 则先构建它;否则若 stmt.Table 非空则写出表名;表名仍为空则先 Parse(stmt.Model);最后如有别名追加 别名
clause.Column
若带表名则写出 表名.;若列名是 PrimaryKey 则用 Schema.PrioritizedPrimaryField.DBName(找不到主键时回退 DBNames[0],Schema 为空报 ErrModelValueRequired);带别名则追加 AS 别名
[]clause.Column
用括号包裹,逗号分隔逐个递归 QuoteTo,形成 (col1,col2)
clause.Expr
交给 v.Build(stmt) 构建表达式
string
交给方言加引号
[]string
括号包裹、逗号分隔的多个引号列
default
用 fmt.Sprint(field) 交给方言处理

关键点:这里大量使用 CurrentTable*)与 PrimaryKey(主键占位符)两个「伪列」,把「当前表」「主键」的解析推迟到 SQL 构建时刻,从而让 Where("id = ?")Order("id") 这类不指定具体列名的写法也能正确工作。

2.5 Quote(field interface{}) string

func(stmt *Statement) Quote(field interface{}) string {
var builder strings.Builder
    stmt.QuoteTo(&builder, field)
return builder.String()
}

一次性拿到带引号的字符串。它新建一个临时 strings.Builder,调用 QuoteTo 后将结果转为 string 返回,不会污染 stmt.SQL。例如 stmt.Quote("users") 得到 `users`(按方言差异)。


三、子句与参数管理

3.1 AddVar(writer clause.Writer, vars ...interface{})

func(stmt *Statement) AddVar(writer clause.Writer, vars ...interface{}) {
for idx, v := range vars {
// ...
switch v := v.(type) {
case sql.NamedArg: ...
case clause.Column, clause.Table: ...
case Valuer: ...
case clause.Interface: ...
case clause.Expression: ...
case driver.Valuer: ...
case []byte: ...
case []interface{}: ...
caseinterface{ getInstance() *DB }: ...
default// reflect 分发
        }
    }
}

把参数写入 SQL(通常写占位符 ?)并登记到 stmt.Vars。逐参数遍历,参数间以 , 分隔。类型分支逐一说明:

  • • sql.NamedArg:仅把 v.Value 追加进 Vars
  • • clause.Column / clause.Table:直接 QuoteTo 写出值,不入 Vars
  • • Valuer(GORM 扩展的值接口):若是指针且为 nil 则写 nil;否则递归调用其 GormValue 产生的值。这里用反射处理了 nil 指针场景。
  • • clause.Interface:构建一个一次性 Clause,合并后 Build(例如把「字段 + 值」结合)。
  • • clause.Expression:调用 v.Build(stmt),由其自行决定写什么、是否登记 Vars。
  • • driver.Valuer / []byte:追加进 Vars,并调用 Dialector.BindVarTo 写占位符。
  • • []interface{}:括号包裹递归 AddVar;空 slice 写 (NULL)
  • • interface{ getInstance() *DB }:即 *DB。用于子查询——若是带 SQL 的子查询(Statement.SQL.Len() > 0),切换 DryRun 把占位符替换为 ? 后作为 clause.Expr/NamedExpr 构建;否则执行一次 Query 回调得到结果。
  • • default:反射分发——slice/array 会转成 (?,?,?)(空则 (NULL)[]byte 特判),基础值则追加 Vars 并 BindVarTo

这是 GORM 参数绑定最复杂的方法,它保证了「值类型、列、表达式、子查询」在进入 SQL 时都能被统一、正确地渲染。

3.2 AddClause(v clause.Interface)

func(stmt *Statement) AddClause(v clause.Interface) {
if optimizer, ok := v.(StatementModifier); ok {
        optimizer.ModifyStatement(stmt)
    } else {
        name := v.Name()
        c := stmt.Clauses[name]
        c.Name = name
        v.MergeClause(&c)
        stmt.Clauses[name] = c
    }
}

把一条子句合并进 stmt.Clauses

  • • 若 v 实现了 StatementModifier,则直接调用 ModifyStatement(stmt)(不作为普通 clause 存储)。
  • • 否则取出已有的同名 Clause(stmt.Clauses[name]),设置其 Name,再调用 MergeClause 把本次内容合并进去,最后存回 map。合并而非覆盖,保证 Where(a).Where(b) 能叠加成 A AND B

3.3 AddClauseIfNotExists(v clause.Interface)

func(stmt *Statement) AddClauseIfNotExists(v clause.Interface) {
if c, ok := stmt.Clauses[v.Name()]; !ok || c.Expression == nil {
        stmt.AddClause(v)
    }
}

仅当同名子句不存在且无表达式时添加。用于设置一个「缺省」子句,避免覆盖用户在链式调用中已设置的值——典型如默认的 LIMIT 或软删除的默认行为。

3.4 BuildCondition(query interface{}, args ...interface{}) []clause.Expression

func(stmt *Statement) BuildCondition(query interface{}, args ...interface{}) []clause.Expression {
// 1. 字符串直传
if s, ok := query.(string); ok {
if _, err := strconv.Atoi(s); err != nil {
if s == "" && len(args) == 0 { returnnil }
iflen(args) == 0 || strings.Contains(s, "?") { return []clause.Expression{clause.Expr{SQL: s, Vars: args}} }
if strings.Contains(s, "@") { return []clause.Expression{clause.NamedExpr{SQL: s, Vars: args}} }
if strings.Contains(strings.TrimSpace(s), " ") { return []clause.Expression{clause.Expr{SQL: s, Vars: args}} }
iflen(args) == 1 { return []clause.Expression{clause.Eq{Column: s, Value: args[0]}} }
        }
    }

// 2. 逐个参数构建 conds
    conds := make([]clause.Expression, 04)
    args = append([]interface{}{query}, args...)
for idx, arg := range args {
// ... 类型分发(value/表达式/DB/map/结构体/slice 等)
    }
// ...
}

把用户传入的 Where/Or/Not 条件归一化为 []clause.Expression。两个阶段:

阶段一:字符串快路径。 若 query 是字符串且不是纯数字(strconv.Atoi 失败),按以下规则快速返回:

  • • 空串且无参数 → nil(无条件);
  • • 无参数或含 ? → 视为 where 条件 clause.Expr{SQL:s, Vars:args}
  • • 含 @ → 命名查询 clause.NamedExpr
  • • 含空格 → 依旧视为 where 条件;
  • • 单参数 → 转为 clause.Eq{Column:s, Value:args[0]}(即 Where("name", "jinzhu"))。

纯数字字符串会「掉出」快路径(Atoi 成功),落入阶段二,从而支持 Where(1) 这种按主键查询。

阶段二:参数归一化。 把 query 并入 args 后逐个处理,类型分发:

  • • clause.Expression / []clause.Expression:直接追加。
  • • *DB:执行其 scopes,提取其 WHERE 子句(若 OrConditions 包裹单个条件则简化为 AndConditions),转为 clause.And(...) 合并。支持 Where(db.Where("a=? ",1)) 的嵌套。
  • • map[interface{}]interface{}:每对 key/value 生成 clause.Eq
  • • map[string]string / map[string]interface{}:先 sort.Strings(keys) 保证顺序稳定,逐个生成 Eq;值为 slice/array 时转为 clause.INkey 含 . 时列名不加表前缀)。
  • • default:结构体 / slice 模式——用 schema.Parse 解析模型,遍历字段;结构体取非零值字段,slice 逐元素取非零字段;默认都生成 Eq。若有 selectedColumns(后面参数为字符串列名)则 restricted=true,只取被选中的列。
  • • 无法解析且反射无效 → ErrInvalidData;否则把 args 作为「主键值列表」生成 clause.IN(即 Where([]int{1,2,3}) 按主键 IN)。

最终统一用 clause.And(conds...) 包裹返回(若 conds 非空),保证多个条件合并为 AND。

3.5 Build(clauses ...string)

func(stmt *Statement) Build(clauses ...string) {
var firstClauseWritten bool
for _, name := range clauses {
if c, ok := stmt.Clauses[name]; ok {
if firstClauseWritten {
                stmt.WriteByte(' ')
            }
            firstClauseWritten = true
if b, ok := stmt.DB.ClauseBuilders[name]; ok {
                b(c, stmt)
            } else {
                c.Build(stmt)
            }
        }
    }
}

按传入的子句名顺序构建最终 SQL。要点:

  • • 子句间以空格分隔,用 firstClauseWritten 标记避免前导空格;
  • • 若 DB.ClauseBuilders 中存在该子句的自定义构建器则优先调用(方言可覆盖默认构建逻辑);
  • • 否则调用子句自身的 Build(stmt)

典型调用:Build("SELECT","FROM","WHERE","ORDER BY","LIMIT"),各子句依次把自己渲染进 stmt.SQL


四、模型解析与元信息

4.1 Parse(value interface{}) (err error)

func(stmt *Statement) Parse(value interface{}) (err error) {
return stmt.ParseWithSpecialTableName(value, "")
}

解析 value 得到 stmt.Schema,内部直接委托给 ParseWithSpecialTableName 且特殊表名为空。

4.2 ParseWithSpecialTableName(value interface{}, specialTableName string) (err error)

func(stmt *Statement) ParseWithSpecialTableName(value interface{}, specialTableName string) (err error) {
if stmt.Schema, err = schema.ParseWithSpecialTableName(value, stmt.DB.cacheStore, stmt.DB.NamingStrategy, specialTableName); err == nil && stmt.Table == "" {
if tables := strings.Split(stmt.Schema.Table, "."); len(tables) == 2 {
            stmt.TableExpr = &clause.Expr{SQL: stmt.Quote(stmt.Schema.Table)}
            stmt.Table = tables[1]
return
        }
        stmt.Table = stmt.Schema.Table
    }
return err
}

解析 value 以填充 stmt.Schema 与 stmt.Table

  1. 1. 调用 schema.ParseWithSpecialTableName 得到 Schema(会利用 cacheStore 缓存,NamingStrategy 决定了实体名→表名的转换)。
  2. 2. 解析成功且 Table 为空时:
    • • 若 Schema.Table 含 .(说明带了 schema 前缀,如 schema1.users),则把整个 schema1.users 存进 TableExpr(用 stmt.Quote 加引号),Table 只保留 users
    • • 否则直接用 Schema.Table 作为 Table

TableExpr 与 Table 分离,保证带 schema 前缀的表在后续 QuoteTo 的 CurrentTable 逻辑里能整体渲染。

4.3 clone() *Statement

func(stmt *Statement) clone() *Statement {
    newStmt := &Statement{ /* 拷贝标量字段 */ }
// 深拷贝 Clauses / Preloads / Joins / scopes / Settings / SQL+Vars
return newStmt
}

深拷贝当前 Statement,用于 GORM 内部的「快照隔离」——例如在 count 查询、嵌套子查询、或需要保留原始上下文时,避免共用 map/slice 造成干扰。要点:

  • • 标量字段直接赋值;
  • • ClausesPreloadsJoinsscopes 均重新分配并拷贝内容(Preloads 的 value slice 是浅拷);
  • • Settingssync.Map)用 Range 逐项 Store 拷贝;
  • • 仅当 SQL.Len() > 0 时才同步 SQL 与 Vars

4.4 SetColumn(name string, value interface{}, fromCallbacks ...bool)

func(stmt *Statement) SetColumn(name string, value interface{}, fromCallbacks ...bool) {
if v, ok := stmt.Dest.(map[string]interface{}); ok {
        v[name] = value
    } elseif v, ok := stmt.Dest.([]map[string]interface{}); ok {
for _, m := range v { m[name] = value }
    } elseif stmt.Schema != nil {
if field := stmt.Schema.LookUpField(name); field != nil {
// 反射写入 Dest 与 ReflectValue ...
        } else {
            stmt.AddError(ErrInvalidField)
        }
    } else {
        stmt.AddError(ErrInvalidField)
    }
}

设置某列的值,注释给出的两种用法:

  • • stmt.SetColumn("Name","jinzhu") —— 在 Hooks 中使用;
  • • stmt.SetColumn("Name","jinzhu", true) —— 在 Callbacks 中使用(fromCallbacks 有值)。

分支逻辑:

  1. 1. Dest 是 map[string]interface{} / []map[string]interface{} → 直接写入 map;
  2. 2. 否则若有 Schema,用 LookUpField 找字段:先通过反射把值写入 Dest 的字段(结构体才允许;不可寻址时用 reflect.New 重建);再根据 ReflectValue 的类型写回——slice/array 时,若 fromCallbacks 有值则遍历写入所有元素,否则只写 CurDestIndex 处的元素;struct 需可寻址。字段不存在报 ErrInvalidField
  3. 3. Schema 为 nil 时报 ErrInvalidField

关键:fromCallbacks 区分「更新单个当前记录」与「批量回调更新所有记录」,与 GORM 批量操作的钩子语义一致。

4.5 Changed(fields ...string) bool

func(stmt *Statement) Changed(fields ...stringbool {
    modelValue := stmt.ReflectValue
switch modelValue.Kind() {
case reflect.Slice, reflect.Array:
        modelValue = stmt.ReflectValue.Index(stmt.CurDestIndex)
    }
    selectColumns, restricted := stmt.SelectAndOmitColumns(falsetrue)
// ... 比较 model 字段值与 Dest 值
}

判断在 Update/UpdateColumn 时模型是否改变(用于触发 BeforeUpdate 前判断是否有可更新字段)。逻辑:

  • • 取当前记录(slice 时取 CurDestIndex 项);
  • • 用 SelectAndOmitColumns(false, true) 拿到「应更新列集合」与是否受限于 Select;
  • • 内嵌闭包 changed(field):取字段当前值;若 Dest 是 map,则比较 map 中同名字段;否则解析 Dest 结构体同名字段,用 utils.AssertEqual 判断是否变化;
  • • fields 为空遍历全部 FieldsByDBName,否则只遍历指定的列名。

v(select 标记为 true)时严格比较;未显式 selected 且 restricted 为真时不视为改变 —— 保证默认全字段更新时「零值=未变化」的判断正确。

4.6 matchName(包级变量)

var matchName = func()func(tableColumn string) (table, column string) {
    nameMatcher := regexp.MustCompile(`^(?:\W?(\w+?)\W?\.)?(?:(\*)|\W?(\w+?)\W?)$`)
returnfunc(tableColumn string) (table, column string) {
// 返回 (table, column);支持 "t.col"、"table.*"、"col"、"*" 形式
    }
}()

用正则把 . 分隔的 表.列 表达式拆成 (table, column) 两部分,支持 * 通配。返回三个分组:table、star(*)、column。若 star 非空则返回 (table, "*")。用于 SelectAndOmitColumns 里识别 users.* 这类带表限定的列。

4.7 SelectAndOmitColumns(requireCreate, requireUpdate bool) (map[string]bool, bool)

func(stmt *Statement) SelectAndOmitColumns(requireCreate, requireUpdate bool) (map[string]boolbool) {
    results := map[string]bool{}
    notRestricted := false
    processColumn := func(column string, result bool) { /* ... */ }

for _, column := range stmt.Selects { processColumn(column, true) }
for _, column := range stmt.Omits  { processColumn(column, false) }

if stmt.Schema != nil {
for _, field := range stmt.Schema.FieldsByName {
// requireCreate: 不可建字段置 false;requireUpdate: 不可更新字段置 false
        }
    }
return results, !notRestricted && len(stmt.Selects) > 0
}

汇总「应选择/应省略」的列集合。返回值:(results map[string]bool, restricted bool)——results 中 true 表示「显式选择」、false 表示「显式省略」;第二个返回值 restricted 表示是否有受限列集合(即是否显式 Select)。

processColumn 处理单列名:

  • • Schema 为 nil → 直接原样记录;
  • • column == "*" → 设置 notRestricted 并把所有 DB 列名都记为 result
  • • column == clause.Associations → 对每个关联名称记为 result
  • • LookUpField 命中 → 记录其 field.DBName
  • • 否则用 matchName 解析 表.列,表匹配当前表时按列记录(* 展开为全部 DB 列);
  • • 其余情况原样记录。

最后若 Schema 非空,根据 requireCreate(不可 Creatable)与 requireUpdate(不可 Updatable)强制把这些字段压成 false

返回的 restricted = !notRestricted && len(Selects) > 0,意味着「只要没写全选且显式 Select 过列」即为受限模式。


五、方法调用关系速览

WriteString / WriteByte ──► 直接写 stmt.SQL
WriteQuoted ──► QuoteTo(writer=&stmt.SQL)
Quote ──► QuoteTo(临时 builder) ──► 返回 string
QuoteTo ──► (§2.4 类型分发) ──► Dialector.QuoteTo / 子表达式 Build

AddClause ──► merge 进 stmt.Clauses,或 StatementModifier 改写
AddClauseIfNotExists ──► 缺省时 AddClause
BuildCondition ──► 各类入参 → []clause.Expression
Build ──► 逐个 clause(或自定义 ClauseBuilder)写进 stmt.SQL

Parse / ParseWithSpecialTableName ──► 填充 Schema / Table / TableExpr
clone ──► 深拷贝快照
SetColumn ──► 写 Dest / ReflectValue 字段
Changed ──► 依赖 SelectAndOmitColumns 判断字段是否变化
SelectAndOmitColumns ──► 依赖 matchName 归一化列集合

六、常见使用场景串联

  1. 1. 链式构建db.Where("age > ?", 18).Order("name").Limit(10) → 各方法 AddClause 把子句塞进 Clauses map。
  2. 2. 执行编译:调用 .Find 等触发回调,最终 Statement.Build("SELECT","FROM",...) 按序渲染出 SQL,AddVar 同时登记 Vars
  3. 3. 条件归一Find 之前会把 Model/Dest 通过 Parse 解析出 Schema,把 where 条件通过 BuildCondition 转成统一表达式。
  4. 4. 字段控制Select/Omit/Schema 权限在 SelectAndOmitColumns 里汇合,供 create/update 回调决定最终写哪些列。

至此,statement.go 中所有数据结构和方法均已覆盖。它虽然在 GORM 中只是「状态容器」,但正是围绕 Statement 的这一组方法,支撑起了 GORM 从「链式 DSL」到「最终 SQL+参数」的全部核心链路。

在Ai的帮助下,对statement.go有了一个全局的源码理解,做个记录,方便后续查阅。

相关学习资料

返回首页浏览学习资料