ARTICLE · 993429
GORM statement.go 源码详解
GORM statement.go 源码详解
文件路径:
gorm.io/gorm/statement.go
相关依赖:clause、logger、schema、utils包
Statement 是 GORM 在生成与执行 SQL 过程中最核心的中间状态载体。它既保存了「用户意图」(Model、Dest、Where 等条件)、又保存了「SQL 渲染结果」(SQL Builder 与参数 Vars),是整个 ORM 引擎的数据中枢。
本文件可大致分为四个部分:
1. 数据类型定义( Statement结构体、join、StatementModifier接口)2. SQL 字符写入与引用( WriteString/WriteByte/WriteQuoted/QuoteTo/Quote)3. 子句与参数管理( AddClause/AddClauseIfNotExists/AddVar/Build/BuildCondition)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):
| 模型与表 | *DBModel、Table、TableExpr、Schema、Dest、ReflectValue |
| SQL 构建 | ClausesBuildClauses、SQL、Vars、Distinct、Selects、Omits、Joins、ColumnMapping |
| 执行控制 | ConnPoolContext、Unscoped、RaiseErrorOnNotFound、SkipHooks、CurDestIndex |
| 关联与数据 | PreloadsSettings、attrs、assigns、scopes |
| 执行结果 | Result |
特别注意嵌入的
*DB字段:它让Statement能直接调用DB的方法(AddError、Session、Dialector、NamingStrategy等),是 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:连接条件(通常是与主表主键等列的匹配条件);• On:ON子句的表达式;• Selects/Omits:JOIN 时附带选择/忽略的列;• Expression:原始 SQL JOIN 表达式(如LEFT JOIN xxx ON ...);• JoinType:LEFT/RIGHT/INNER等类型。
Joins 字段([]join)以 slice 形式保存多条 JOIN,供构建 FROM 子句时使用。
1.3 StatementModifier 接口
type StatementModifier interface {
ModifyStatement(*Statement)
}如果一个 clause.Interface 额外实现了该接口,那么它就不是「往某个子句里合并」,而是直接修改整个 Statement。这是 GORM 为少数特殊子句(如 clause.Locking、clause.For)提供的自治改写入口(在 AddClause 中判断)。
二、SQL 字符写入与引用
2.1 WriteString(str string) (int, error)
func(stmt *Statement) WriteString(str string) (int, error) {
return stmt.SQL.WriteString(str)
}把一段未经处理的字符串直接追加到 stmt.SQL(strings.Builder)中,返回写入字节数与错误。几乎所有 SQL 片段(关键字、表名、=、( 等)都经由它落进 SQL Builder。
2.2 WriteByte(c byte) error
func(stmt *Statement) WriteByte(c byte) error {
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, 0, 4)
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.IN(key含.时列名不加表前缀)。• 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. 调用 schema.ParseWithSpecialTableName得到Schema(会利用cacheStore缓存,NamingStrategy决定了实体名→表名的转换)。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 造成干扰。要点:
• 标量字段直接赋值; • Clauses、Preloads、Joins、scopes均重新分配并拷贝内容(Preloads的 value slice 是浅拷);• Settings(sync.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. Dest是map[string]interface{}/[]map[string]interface{}→ 直接写入 map;2. 否则若有 Schema,用LookUpField找字段:先通过反射把值写入Dest的字段(结构体才允许;不可寻址时用reflect.New重建);再根据ReflectValue的类型写回——slice/array 时,若fromCallbacks有值则遍历写入所有元素,否则只写CurDestIndex处的元素;struct 需可寻址。字段不存在报ErrInvalidField。3. Schema为 nil 时报ErrInvalidField。
关键:
fromCallbacks区分「更新单个当前记录」与「批量回调更新所有记录」,与 GORM 批量操作的钩子语义一致。
4.5 Changed(fields ...string) bool
func(stmt *Statement) Changed(fields ...string) bool {
modelValue := stmt.ReflectValue
switch modelValue.Kind() {
case reflect.Slice, reflect.Array:
modelValue = stmt.ReflectValue.Index(stmt.CurDestIndex)
}
selectColumns, restricted := stmt.SelectAndOmitColumns(false, true)
// ... 比较 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]bool, bool) {
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. 链式构建: db.Where("age > ?", 18).Order("name").Limit(10)→ 各方法AddClause把子句塞进Clausesmap。2. 执行编译:调用 .Find等触发回调,最终Statement.Build("SELECT","FROM",...)按序渲染出 SQL,AddVar同时登记Vars。3. 条件归一: Find之前会把Model/Dest通过Parse解析出Schema,把 where 条件通过BuildCondition转成统一表达式。4. 字段控制: Select/Omit/Schema权限在SelectAndOmitColumns里汇合,供 create/update 回调决定最终写哪些列。
至此,statement.go 中所有数据结构和方法均已覆盖。它虽然在 GORM 中只是「状态容器」,但正是围绕 Statement 的这一组方法,支撑起了 GORM 从「链式 DSL」到「最终 SQL+参数」的全部核心链路。
在Ai的帮助下,对statement.go有了一个全局的源码理解,做个记录,方便后续查阅。