go-gorm源码分析(一)

October 15, 2024

最近在查找一些golang版本的SQL Builder的选型,刚好了解到了gorm的能力。gorm内嵌的SQL拼接能力其实不太适合作为独立特性来使用,其内部实现与gorm有较深的耦合关系。但是gorm的关系操作能力很强大,猜测跟directus中的实现有些类似。刚好之前阅读directus源码时觉得ts代码有些繁杂晦涩,所以阅读一遍gorm的源码,并记录其分析过程。

源码内容较大,相关分析会分几批来进行。

简介

GORM是golang下的ORM库,主要能力包括:

  • ORM能力:即通过golang中的object(常见为struct)能够与DB表中的记录一一映射,通过操作对象打到快速读写DB的能力。
  • 关联关系:实现了BelongTo(M2O)、HasMany(O2M)、HasOne(O2O)、ManyToMany(M2M)等常见的关系。即通过一次API调用,实现关联表的相关读写操作,避免在业务逻辑中维护关联关系。
  • Create,Save,Update,Delete,Find 中钩子方法:可定义struct的各种操作hook,在DB操作前后对写入、读取的数据进行格式化等处理。
  • 内嵌SQL builder,支持select、join、clause设置等。

更多特性以及用法见官方文档

准备

在准备分析之前,需要准备一些测试代码,用官方的demo就可以,在本地使用docker搭建mysql环境,然后直接运行相关代码。注意点:

  • 开启日志

通过配置开启gorm的日志,可以看到其内部的一些输出。除了错误信息外(默认会开启),目前仅有执行SQL语句的输出,同时也包括其执行耗时、rowsAffected等明细数据。

[go]
// callback execute结尾有输出相关API调用对应的SQL,以及耗时等信息。
if stmt.SQL.Len() > 0 {
	db.Logger.Trace(stmt.Context, curTime, func() (string, int64) {
		sql, vars := stmt.SQL.String(), stmt.Vars
		if filter, ok := db.Logger.(ParamsFilter); ok {
			sql, vars = filter.ParamsFilter(stmt.Context, stmt.SQL.String(), stmt.Vars...)
		}
		return db.Dialector.Explain(sql, vars...), db.RowsAffected
	}, db.Error)
}

// 输出的日志格式:
// [4.347ms] [rows:1] SELECT * FROM `t_User` WHERE `t_User`.`DeletedAt` IS NULL ORDER BY `t_User`.`ID` LIMIT ?

注意:日志输出顺序不等于sql执行顺序。比如在部分关系的处理上,对Relation表的查询需要依赖Primary的查询结果id。但是从日志上看可能会看到Relation表日志先输出。实际在执行调度上是通过callback类似机制触发,不过日志打印先行执行而已。

此外,为了避免对DB的影响,gorm也提供了dryRun模式,仅用于输出SQL。

  • 数据结构查看方法

gorm一些核心数据结果阅读代码可能比较隐晦,尤其各种reflect的代码充斥其中。可以通过调用其非公开API并打印日志,来观察其核心数据结构。比如下面代码演示如何查看核心数据结schema、fields。

[go]

func SchemaInfo(db *gorm.DB) {
	user := User{}

	db.Debug().First(&user)
	
	// 参考execute中的代码,根据struct结构解析其schema以及fields
	sch, err := schema.Parse(&User{}, &sync.Map{}, db.Config.NamingStrategy)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(sch.Name)
	fmt.Println(sch.ModelType)
	fmt.Println(sch.Table)
	fmt.Println(sch.PrimaryFieldDBNames)
	fmt.Println("CreateClauses:", sch.CreateClauses)
	fmt.Println("QueryClauses:", sch.QueryClauses)
	for _, c := range sch.QueryClauses {
		fmt.Println("QueryClauses Type:", reflect.TypeOf(c))
	}

	for i, f := range sch.Fields {
		fmt.Printf("-----------%d-----------\n", i)
		fmt.Println("DBName: ", f.DBName)
		fmt.Println("Name: ", f.Name)
		fmt.Println("BindNames: ", f.BindNames)
		fmt.Println("DataType: ", f.DataType)
		fmt.Println("FieldType: ", f.FieldType)
		fmt.Println("StructField: ", f.StructField)
		fmt.Println("BindName: ", f.BindName())
	}
}

DB配置以及连接管理

配置方式

gorm的配置及使用方式可以见官方文档

从代码中可以看出,option是采用golang的Functional Options模式来实现,支持指定多个option。

[go]
// Option gorm option interface
type Option interface {
	Apply(*Config) error
	AfterInitialize(*DB) error
}

func Open(dialector Dialector, opts ...Option) (db *DB, err error) {
	config := &Config{}
  // ...
	for _, opt := range opts {
		if opt != nil {
			if applyErr := opt.Apply(config); applyErr != nil {
				return nil, applyErr
			}
			defer func(opt Option) {
				if errr := opt.AfterInitialize(db); errr != nil {
					err = errr
				}
			}(opt)
		}
	}
	// ...
}

常见的配置项

  • SkipDefaultTransaction

是否禁用默认transcation,默认为false。及所有变更操作(create、update、delete)默认会开启事务(因为可能会涉及关系表处理等多次DB交互的场景)。禁用默认事务会带来性能的提升,但会影响数据一致性。

事务生效过程与其他db相关操作一样,都是通过callback的方式注册到写操作的各个 processor 的callback链条中。在 proces.Execute() 中被调度。

注册:default callback注册时,会根据此配置决定是否开启事务callback。

[go]
createCallback := db.Callback().Create()
createCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
// other callbacks
createCallback.Match(enableTransaction).Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction)

callback函数:

[go]
// 事务callback中执行DB事务开启
func BeginTransaction(db *gorm.DB) {
	if !db.Config.SkipDefaultTransaction && db.Error == nil {
		if tx := db.Begin(); tx.Error == nil {
			db.Statement.ConnPool = tx.Statement.ConnPool
			db.InstanceSet("gorm:started_transaction", true)
		} else if tx.Error == gorm.ErrInvalidTransaction {
			tx.Error = nil
		} else {
			db.Error = tx.Error
		}
	}
}

// 根据db执行结果commit or callback
func CommitOrRollbackTransaction(db *gorm.DB) {
	if !db.Config.SkipDefaultTransaction {
		if _, ok := db.InstanceGet("gorm:started_transaction"); ok {
			if db.Error != nil {
				db.Rollback()
			} else {
				db.Commit()
			}

			db.Statement.ConnPool = db.ConnPool
		}
	}
}
  • NamingStrategy

用于处理命名映射,比如golang结构体字段名(仅允许大写开头的字母)和db表中列名的映射等。

  • DryRun

DryRun模式开关,用于调试SQL。

  • FullSaveAssociations

更新关联表,gorm更新时默认不更新关系表字段,需要时通过全局配置或者session开启。

  • PrepareStmt

是否启用golang sql的 prepared-statements 模式。即开启后db的执行会通过 Stmt 模式执行,适用于批量或者多goroutine并存的场景。

  • DisableNestedTransaction
  • AllowGlobalUpdate

GORM 默认不允许进行全局 update/delete,该操作会返回 ErrMissingWhereClause 错误。在Query、Delete callback中会校验where条件是否满足(soft delete条件除外)。

[go]
func checkMissingWhereConditions(db *gorm.DB) {
	if !db.AllowGlobalUpdate && db.Error == nil {
		where, withCondition := db.Statement.Clauses["WHERE"]
		if withCondition {
			if _, withSoftDelete := db.Statement.Clauses["soft_delete_enabled"]; withSoftDelete {
				whereClause, _ := where.Expression.(clause.Where)
				withCondition = len(whereClause.Exprs) > 1
			}
		}
		if !withCondition {
			db.AddError(gorm.ErrMissingWhereClause)
		}
		return
	}
}

连接池

默认情况下,gorm会使用gorm下各dialector提供的连接池实现,以mysql为例,使用的:

[go]
func (dialector Dialector) Initialize(db *gorm.DB) (err error) {
	if dialector.DriverName == "" {
		dialector.DriverName = DefaultDriverName
	}

	if dialector.DefaultDatetimePrecision == nil {
		dialector.DefaultDatetimePrecision = &defaultDatetimePrecision
	}

	if dialector.Conn != nil {
	  // 优先使用适用方传入的连接池。通常是 go-gorm/mysql
		db.ConnPool = dialector.Conn
	} else {
	  // 否则内部使用 go-sql-driver/mysql 
		db.ConnPool, err = sql.Open(dialector.DriverName, dialector.DSN)
		if err != nil {
			return err
		}
	}
}

用户也可以实现自己的连接池,只要满足接口要求,实现CRUD相关功能即可。

[go]
// ConnPool db conns pool interface
type ConnPool interface {
  // prepare模式使用
	PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
	// Exec executes a query without returning any rows.
	ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
	// QueryContext executes a query that returns rows, typically a SELECT.
	QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
	// QueryRowContext executes a query that is expected to return at most one row. QueryRowContext always returns a non-nil value. Errors are deferred until Row's Scan method is called.
	QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
}

连接池还有另外一种方式,如果开启了 prepared-statements 模式,gorm会在 sql.DB上层进行一次封装( PreparedStmtDB),其目的有二:

  • 屏蔽stmt相关的逻辑,方便CRUD操作。即在执行相关sql前不再需要上层执行prepare stmt后才能执行query。
  • 性能考虑,在stmt连接池中会在内存中自动缓存prepare的结果(连接有效),key为gorm拼接后的SQL。
[go]
type Stmt struct {
	*sql.Stmt
	Transaction bool
	prepared    chan struct{} // 多goroutine同步使用
	prepareErr  error
}
// 在 sql.DB 之上的连接池
type PreparedStmtDB struct {
	Stmts map[string]*Stmt  // stmts缓存
	Mux   *sync.RWMutex  // 缓存锁
	ConnPool
}

// Open initialize db session based on dialector
func Open(dialector Dialector, opts ...Option) (db *DB, err error) {

	// 是否启用 prepare 模式
	if config.PrepareStmt {
		preparedStmt := NewPreparedStmtDB(db.ConnPool)
		db.cacheStore.Store(preparedStmtDBKey, preparedStmt)
		// 倘若启用了 prepare 模式,会对 connPool 进行替换
		db.ConnPool = preparedStmt
	}
}

会话以及链式调用

gorm的API分位三类:

  • Chain Methods:对应到源码中的 chainable_api.go ,通常用户SQL执行前的各种条件。比如 Where、GroupBy、Sort等。
  • Finisher Methods:执行SQL,参见 finisher_api.go 。在finisher_api中会使用Chain Methods增加新的条件,比如 First 仅取一条数据。
  • New Session Methods: 用于创建一个安全的可重用的会话。

先看一个官方的例子

[go]
// 使用Where获取到了一个gorm.Statement
queryDB := DB.Where("name = ?", "jinzhu")

// 首次查询,条件会被加入到gorm.Statement
queryDB.Where("age > ?", 10).First(&user)
// SQL: SELECT * FROM users WHERE name = "jinzhu" AND age > 10

// 二次查询,条件也会被继续追加到同一个gorm.Statement
queryDB.Where("age > ?", 20).First(&user2)
// SQL: SELECT * FROM users WHERE name = "jinzhu" AND age > 10 AND age > 20

使用 queryDB 进行了两次查询操作,但是第二次查询时会带入第一次查询通过链式调佣指定的条件,导致sql污染问题的产生,分析原因之前先了解下链式调用的原理。

链式调用原理

在DB结构体中,存在一个Statment对象,来保存一次会话所有查询条件。

[go]
type DB struct {
	*Config
	Error        error
	RowsAffected int64
	// 会话状态信息
	Statement *Statement
	// 克隆次数
	clone int
}

// 链式调用时,各种条件会被加入到Stament
db.Where("name = ?", "jinzhu").Where("age = ?", 18).Find(&users)

为实现链式调用,在多次调用Chain Methods时,各种条件都会被加入到同一个Statment中,并最终被执行。所以上面的SQL污染问题的原因也在于此。在使用 DB.Where() 获取到DB对象 queryDB 后,后续使用同一个 queryDB 多次调用,Chain Methods指定的条件会被累加到一个Statment中。

但是如果直接使用 gorm.Open 返回的db对象,却没出现污染问题:

[go]
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// 'db' is a newly initialized `*gorm.DB`, which is safe to reuse.

db.Where("name = ?", "jinzhu").Where("age = ?", 18).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age = 18;

db.Where("name = ?", "jinzhu2").Where("age = ?", 20).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu2' AND age = 20;

原因就在于Chain Method的处理逻辑上,对于二者存在区别。以Where为例:

[go]
func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB) {
	tx = db.getInstance()
	if conds := tx.Statement.BuildCondition(query, args...); len(conds) > 0 {
		tx.Statement.AddClause(clause.Where{Exprs: conds})
	}
	return
}

在Where内部,不是直接使用db对象的Statment,而是先通过getInstance获取到一个“新的”db对象。这里的新不一定会new一个对象,也有可能会复用。

[go]

func (db *DB) getInstance() *DB {
	if db.clone > 0 {
		// 新建一个 db 实例以及statment,clone为0
		// 即首次调用 Chain Method 时,会走到此逻辑。
		tx := &DB{Config: db.Config, Error: db.Error}

		// 倘若是首次对 db 进行 clone,则需要构造出一个新的 statement 实例。由于其clone值为0,后续的调用会被复用。
		if db.clone == 1 {
			// clone with new statement
			tx.Statement = &Statement{
				DB:        tx,
				ConnPool:  db.Statement.ConnPool,
				Context:   db.Statement.Context,
				Clauses:   map[string]clause.Clause{}, // 新建的 statement 实例中,clause为空 map。表示后续的链式操作,都会基于该 statement 实例进行。
				Vars:      make([]interface{}, 0, 8),
				SkipHooks: db.Statement.SkipHooks,
			}
			if db.Config.PropagateUnscoped {
				tx.Statement.Unscoped = db.Statement.Unscoped
			}
		} else {
			// with clone statement
			// Session、WithContext、Debug 等操作,会复用该 db 实例的 statement 实例。将该 db 实例的 statement 实例中的 clause、vars 等属性,复制到新的 db 实例中。
			tx.Statement = db.Statement.clone()
			tx.Statement.DB = tx
		}

		return tx
	}

	// 当clone小于或等于0,直接返回db本身;
	// 同一个db对象,第二次调用Chain Method时,会走到此逻辑。复用Statment
	return db
}

可见对于使用 gorm.Open 获取到的db对象和使用 Where 获取到db对象,根据其内部的clone计数标识,在调用后续的 Chain Method时会产生不同的效果。

  • 前者:gorm.Open 初始化时clone为1,此时首次调用getInstance会命中新建逻辑。会新建一个statment对象(clone=0),供后续的调用复用(clone 标记为0)。
  • 后者:由于之前已经通过 Chain Method 获取到了一份复制过的statment(clone值为0),后续的链式调用也会继续复用同一份。

类似数结构,gorm.Open 创建的DB对象可以被多次复制,并产生祖孙节点。所以为了避免污染,尽量使root节点本身或者使用会话模式。

会话模式

如何安全的复用statment以达到条件重复利用的目的呢?gorm提供了会话模式来作为解决方案。参考官方demo:

[go]
queryDB := DB.Where("name = ?", "jinzhu").Session(&gorm.Session{})

// First query
queryDB.Where("age > ?", 10).First(&user)
// SQL: SELECT * FROM users WHERE name = "jinzhu" AND age > 10

// Second query, safely isolated
queryDB.Where("age > ?", 20).First(&user2)
// SQL: SELECT * FROM users WHERE name = "jinzhu" AND age > 20

DB.Session() 会复制一份db对象,并将clone标识为2。如上,在Chain Method中调用getInstance时,会命中Statment的clone逻辑。此时虽然也会新建db、Statment,不同的是会将Statment中所有之前的数据copy一份到新建的db.Statment对象中。后续的每次Query调用也都会产生一次新增、copy操作实现类似条件继承的效果,而非直接复用或者纯新增。避免了污染的可能性。

SQL构建

gorm支持Raw SQL,更常见的用法是使用其SQL Builder能力来构建SQL。

[go]
// 使用 GORM API 构建 SQL
rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows()
defer rows.Close()
for rows.Next() {
  rows.Scan(&name, &age, &email)

  // 业务逻辑...
}

// RAW SQL
rows, err := db.Raw("select name, age, email from users where name = ?", "jinzhu").Rows()
defer rows.Close()
for rows.Next() {
  rows.Scan(&name, &age, &email)

  // 业务逻辑...
}

Statment

SQL Builder的实现原理与其他实现差别不大,比如 go-sqlbuilder。本质上都是通过API设置好目的SQL的各部分参数,最后Build输出完成的SQL语句,具体可设置内容参见Statment的定义:

[go]

// Statement statement
type Statement struct {
	*DB
	TableExpr *clause.Expr
	// 表名
	Table     string
	// 操作的 po(persist object) 模型
	Model    interface{}
	Unscoped bool
	// 处理结果反序列化到此处
	Dest         interface{}
	ReflectValue reflect.Value
	// 各种条件语句,通过 AddClause 添加到Statement
	Clauses      map[string]clause.Clause
	// 从 processor 中copy而来的操作相关的clause名称
	BuildClauses []string
	// 是否启用 distinct 模式
	Distinct bool
	// select 语句
	Selects []string // selected columns
	// omit 语句
	Omits         []string          // omit columns
	ColumnMapping map[string]string // map columns
	// Join 语句
	Joins    []join
	Preloads map[string][]interface{}
	Settings sync.Map
	// 连接池,通常情况下是 database/sql 库下的 *DB  类型.  在 prepare 模式为 gorm.PreparedStmtDB
	ConnPool ConnPool
	// 操作表的概要信息
	Schema  *schema.Schema
	Context context.Context
	// 在未查找到数据记录时,是否抛出 recordNotFound 错误
	RaiseErrorOnNotFound bool
	SkipHooks            bool
	SQL                  strings.Builder
	Vars                 []interface{}
	CurDestIndex         int
	attrs                []interface{}
	assigns              []interface{}
	scopes               []func(*DB) *DB
}

重点结构:

  • Table:指定查询表名称。通过 db.Table() 设置。 db.Table("users")
  • Model:指定数据模型,通过 db.Model() 设置。

在构建SQL之前,如果指定了Model,则会根据其结构定义,使用 golang reflect 相关能力,解析出DB相关表结构信息。

[go]
	// callback.Execute() 中会根据model解析出Schema
	if stmt.Model != nil {
		if err := stmt.Parse(stmt.Model); err != nil && (!errors.Is(err, schema.ErrUnsupportedDataType) || (stmt.Table == "" && stmt.TableExpr == nil && stmt.SQL.Len() == 0)) {
			if errors.Is(err, schema.ErrUnsupportedDataType) && stmt.Table == "" && stmt.TableExpr == nil {
				db.AddError(fmt.Errorf("%w: Table not set, please set it like: db.Model(&user) or db.Table(\"users\")", err))
			} else {
				db.AddError(err)
			}
		}
	}

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

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
}

ParseWithSpecialTableName实现Schema核心解析逻辑,大致原理是使用golang的反射机制,解析struct定义,以及根据各字段的tag信息,解析出DB中字段的详细信息(数据类型、PrimaryKey、AutoIncrement、是否允许为空、默认值等),以及其与内存中的结构体字段的映射关系,以供后续的CRUD操作(QueryContext、ExecuteContext)使用。此部分内容后面详细分析。

  • scopes

scopes用于存储一些需要动态设置Statment的callback函数,通常用于设置动态条件。各callback会在sql生成前被执行。

[go]
func (db *DB) executeScopes() (tx *DB) {
	scopes := db.Statement.scopes
	db.Statement.scopes = nil
	for _, scope := range scopes {
		db = scope(db)
	}
	return db
}

Clauses

*gorm.Statement 底层实际使用 Clause 列表来生成SQL,暴露的各种API基本上也是对Clause 的操作。从代码目录上看,clause基本就是对SQL各部分拆解后的封装,包括:Insert/Delete/Select/Update、From、Join、Where、GroupBy、OrderBy等。Clause 的接口要求:

[go]
type Interface interface {
	Name() string        // clause 名称
	Build(Builder)       // 生成对应的 sql 部分
	MergeClause(*Clause) // 和同类 clause 合并
}

gorm中支持的 Clause 类型包括:

[go]
func (d Delete) Name() string {
        return "DELETE"
}
func (from From) Name() string {
        return "FROM"
}
func (groupBy GroupBy) Name() string {
        return "GROUP BY"
}
func (insert Insert) Name() string {
        return "INSERT"
}
func (limit Limit) Name() string {
        return "LIMIT"
}
func (locking Locking) Name() string {
        return "FOR"
}
func (OnConflict) Name() string {
        return "ON CONFLICT"
}
func (orderBy OrderBy) Name() string {
        return "ORDER BY"
}
func (returning Returning) Name() string {
        return "RETURNING"
}
func (s Select) Name() string {
        return "SELECT"
}
func (set Set) Name() string {
        return "SET"
}
func (update Update) Name() string {
        return "UPDATE"
}
func (Values) Name() string {
        return "VALUES"
}
func (where Where) Name() string {
        return "WHERE"
}

 

在上层设置好各种Clause之后,何时触发SQL的构建呢,以及如何处理各DB的差异?

以Mysql下的插入场景为例,观察其SQL构建过程。

  1. 初始化:在dialector的初始化过程中( gorm.Open() ),会注册cRUD的各processor所需的callback以及相关配置
[go]
// callback配置
type Config struct {
	LastInsertIDReversed bool
	CreateClauses        []string
	QueryClauses         []string
	UpdateClauses        []string
	DeleteClauses        []string
}

// mysql dialactor传入的配置参数,指定各Processor所需的clause名称,供Statment.Build时使用

// CreateClauses create clauses
CreateClauses = []string{"INSERT", "VALUES", "ON CONFLICT"}
// QueryClauses query clauses
QueryClauses = []string{}
// UpdateClauses update clauses
UpdateClauses = []string{"UPDATE", "SET", "WHERE", "ORDER BY", "LIMIT"}
// DeleteClauses delete clauses
DeleteClauses = []string{"DELETE", "FROM", "WHERE", "ORDER BY", "LIMIT"}

可见各dialector会决定其自身各操作所需的 Clause 名称,来选择其组建SQL所需的 Clause 条件,然后RegisterDefaultCallbacks 中注册各类CRUD操作的callback逻辑,以插入为例,回调操作(callbacks)包括:

[go]
// 设置写操作相关回调 以及 配置
createCallback := db.Callback().Create()
// 开启事务
createCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
// 业务回调: BeforeSave、BeforeCreate 
createCallback.Register("gorm:before_create", BeforeCreate)
// 处理插入前的关系:仅 BelongTo 关系
createCallback.Register("gorm:save_before_associations", SaveBeforeAssociations(true))
// 插入SQL构建及执行
createCallback.Register("gorm:create", Create(config))
// 查理插入后关系:包括 HasOne、HasMany、Many2Many 关系
createCallback.Register("gorm:save_after_associations", SaveAfterAssociations(true))
// 业务回调:AfterCreate、AfterSave
createCallback.Register("gorm:after_create", AfterCreate)
// 结束事务
createCallback.Match(enableTransaction).Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction)
// 设置dialector所需 Clause 名称
createCallback.Clauses = config.CreateClauses
  1. gorm中的各个 Finisher Methods 会设置Dest,然后触发callbacks链的执行。包括上面代码中的各个callbacks。
  2. 其中 Create callback负责SQL构建以及数据的写入。
[go]
if db.Statement.SQL.Len() == 0 {
	db.Statement.SQL.Grow(180)
	db.Statement.AddClauseIfNotExists(clause.Insert{})
	// 值转换,并添加必要的子句
	db.Statement.AddClause(ConvertToCreateValues(db.Statement))

	// 构建SQL语句:BuildClauses 即为dialector中指定的processor所需 Clause 名称
	db.Statement.Build(db.Statement.BuildClauses...)
}

BuildClause 即为dialector中所指定构建Mysql插入语句时支持的clause名称,即上面的 "INSERT", "VALUES", "ON CONFLICT" 三部分。

最后的Build就比较简单了,取出Statment.Clause 中的各个Clause,调用其Build方法构建SQL子句,最后合并成完整的SQL。

[go]
func (stmt *Statement) Build(clauses ...string) {
	var firstClauseWritten bool

	for _, name := range clauses {
		if c, ok := stmt.Clauses[name]; ok {
			if firstClauseWritten {
				stmt.WriteByte(' ') // add space between clauses
			}

			firstClauseWritten = true
			if b, ok := stmt.DB.ClauseBuilders[name]; ok {
				// dialector 自定义的 clause 构建方法
				b(c, stmt)
			} else {
				// clause对应的build方法
				c.Build(stmt)
			}
		}
	}
}

其中,允许dialector自定义clause的Build实现。比如mysql就自定义了 ON CONFLICT 等的实现

 

See all postsSee all posts