Go第三方框架--gorm框架(二)

增删改查(dml操作)

查询操作

gorm查询主要执行了三种操作:

  1. 通过链式函数调用累计查询条件(在map[string]clause.Clause中累计)
  2. 将查询条件转换成sql(赋值给 Statement.SQL和Statement.Vals)
  3. 执行对应回调函数
    我们以下面简单例子来进行说明:
go 复制代码
func TestGorm(t *testing.T) {

	//gorm
	dsn := "root:root@tcp(127.0.0.1:3306)/world?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err)
	}

	var userinfo Userinfo
	// 其中 操作1 主要涉及  db.Table("userinfos").Where("name = ?", "lisan").Where("hobby","reading");和 .First(&userinfo)(First里主要累计 order by 主键和 limit 1条件)
	// 操作2,3   在.First(&userinfo) 函数完成。
	if err = db.Table("userinfos").Where("name = ?", "lisan").Where("hobby = ?","reading").First(&userinfo).Error; err != nil {
		return
	}
	 }

我们来逐步进行下讲解(dml操作基本都是这个流程)

累计查询条件

我们先来看下整个累计的效果吧,我们在First函数的 第一行加上断点,可以得到如下的sql累计效果。

我们在语句中看到了三种关键字 Where,limit,order by,则Stement的 Clauses 的key是这三个value 是对应的语句和值。我们来看下是不是

可以看到 绿色框里是key,红色框里ddl操作语句相关的数组,其是value的核心属性。

通过上面的debug,基本验证了我们的猜测。

接下来我们来从代码的角度梳理下,怎么一步步形成上面的map (map[string]clause.Clause)

首先是 db. db.Table("userinfos"),我们来看下 源码:

go 复制代码
// 指定需要操作的表 这里会复制一份 db ,不影响其他的dml操作
func (db *DB) Table(name string, args ...interface{}) (tx *DB) {
	tx = db.getInstance() // 这边会复制一份 db,后续链式调用的sql变量都会累积到这个db.Statement上 ,新的db的clone没有赋值为0, 再调用 getInstance时 就都是返回自身
	if strings.Contains(name, " ") || strings.Contains(name, "`") || len(args) > 0 {
		tx.Statement.TableExpr = &clause.Expr{SQL: name, Vars: args}
		if results := tableRegexp.FindStringSubmatch(name); len(results) == 3 {
			if results[1] != "" {
				tx.Statement.Table = results[1]
			} else {
				tx.Statement.Table = results[2]
			}
		}
	} else if tables := strings.Split(name, "."); len(tables) == 2 {
		tx.Statement.TableExpr = &clause.Expr{SQL: tx.Statement.Quote(name)}
		tx.Statement.Table = tables[1]
	} else if name != "" {
		tx.Statement.TableExpr = &clause.Expr{SQL: tx.Statement.Quote(name)}
		tx.Statement.Table = name
	} else {
		tx.Statement.TableExpr = nil
		tx.Statement.Table = ""
	}
	return
}

其中 db.getInstance() 函数控制db实例的获取方式,如下:

go 复制代码
// 获取一个db实例,0:原始db ; 1: 复制一份,不同ddl,dml操作使用 ,会复制连接池 注册的回调函数等; 2:开启事务时使用
func (db *DB) getInstance() *DB {
	if db.clone > 0 {
		tx := &DB{Config: db.Config, Error: db.Error}
		
		// 等于1 则 Statement 需要重新生成一份,避免不同dml/ddl操作互相影响
		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{}, 
				Vars:      make([]interface{}, 0, 8),
				SkipHooks: db.Statement.SkipHooks,
			}
			// 开启事务 todo
		} else {
			// with clone statement
			tx.Statement = db.Statement.clone()
			tx.Statement.DB = tx
		}

		return tx
	}
	// 等于0 直接返回db自身 
	return db
}

db.Table( "userinfos")初始化了一个新的 db(含新的Statement),后续链式操作行为都在这个新db上累加,做到不同语句不互相影响;指定了需要查询的表。

继续执行后续语句: db.Table("userinfos").Where("name = ?", "lisan")

我们看下源码:

go 复制代码
func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB) {
	tx = db.getInstance() //  db.clone是0 返回自身 用来累加sql
	// 将sql条件 转换成拼装结构体数组的参数 赋值给 []clause.Expression 
	if conds := tx.Statement.BuildCondition(query, args...); len(conds) > 0 {

		// 将where 条件 添加到 Key 是where的map中(conds添加到数组中,数组用来累加后续 where查询条件) (这边实现了where条件的积累,不同的dml操作AddClause实现不一样,但都是向对应关键字key添加value)
		tx.Statement.AddClause(clause.Where{Exprs: conds})
	}
	return
}

db.Table("userinfos").Where("name = ?", "lisan").Where("hobby = ?", "reading")时,其结构如下图1所示:

继续执行到First(...) 函数第一行其结构如下图2:

我们看到 key是字符串形式的 value是一个接口,结构体如下:

go 复制代码
type Interface interface {
	Name() string   // 获取关键字名字 用来生成key
	Build(Builder)  // todo 
	MergeClause(*Clause) // 添加key对应的value(sql语句)
}

几乎所有dml关键字都实现了这个接口,我们来看下有哪些,如下图:

我们来看下Where的实现逻辑,源码如下(其他的dml操作可自行查看):

go 复制代码
func (stmt *Statement) AddClause(v clause.Interface) {
	if optimizer, ok := v.(StatementModifier); ok {
		optimizer.ModifyStatement(stmt)
	} else {
		name := v.Name() // 获取关键字 wHERE
		c := stmt.Clauses[name] // 获取对应的结构体
		c.Name = name // 赋值
		v.MergeClause(&c) // 添加 本sql结构体 (v中包含sql)到对应v中的数组中
		stmt.Clauses[name] = c // 赋值map
	}
}

其中v.MergeClause(&c)代码如下:

go 复制代码
// MergeClause merge where clauses
func (where Where) MergeClause(clause *Clause) {
	if w, ok := clause.Expression.(Where); ok {

		//  深copy
		exprs := make([]Expression, len(w.Exprs)+len(where.Exprs))
		copy(exprs, w.Exprs)
		copy(exprs[len(w.Exprs):], where.Exprs)
		where.Exprs = exprs // 将对应sql结构体 存入数组
	}
	// 赋值给 Exoression 到这里 map[string]clause.Clause key:Whereh 对应的 value就赋值完毕了
	clause.Expression = where
}

这样就形成了如结构图1所示的结构。其他的关键字累加逻辑基本一致,不在赘述,经过不断累加就能形成如结构图2所示结构。

总结:每个dml要想加入map基本都需要两步 1:sql语句处理 2:加入value对应的结构体参数中。

查询条件转sql

上面dml关键字和对应的sql value结构体组合成了map[string]clause.Clause,下面就是将 map[string]clause.Clause转换成sql。到这里可能会有疑问,map中就三个关键字啊,缺少关键的SELECT 和 FROM。剩下的两个关键字,由于是查询语句必备的,所以会在转换成sql的函数中给自动添加上。

查询条件map[string]clause.Clause 转sql的涉及的函数调用链如下:

// from 的value 中 tables为啥是 nil那 sql语句累计时 从 db.statement.Tables处获取值

添加上 SELECT 和 FROM 后的完整示意图如下:

完整的map组合完毕后,接下来就开始组装原生sql,我们都知道select语句的关键字出现的先后顺序是一定的,所以我们需要有一个关键字先后顺序列表来约束sql语句生成过程中出现的顺序,这就是 Statement的BuildClauses关键字,这个关键字在执行db.open(...)初始化注册回调函数时会初始化,各个dml都有一个特定的数组。如下:

讲完大致的执行流程我们来看下BuildQuerySQL(...)源码:

go 复制代码
func BuildQuerySQL(db *gorm.DB) {
		
			// ... 
			db.Statement.AddClause(fromClause)
		} else {
			//  map[string]clause.Clause中添加 FROM关键字
			db.Statement.AddClauseIfNotExists(clause.From{})
		}

		//  map[string]clause.Clause中添加 SELECT关键字
		db.Statement.AddClauseIfNotExists(clauseSelect)
		// BuildClauses 组合sql时 需要的关键字 按照先后顺序来组合 比如 先是   map[SELECT] 参与组合SQL语句
 		db.Statement.Build(db.Statement.BuildClauses...)
	}
}

可以看到 在map中加入 SELECT和 FROM关键字后,开始执行Build(...)函数来构造sql语句,根据map[string]clause.Clause来构造,Statement.SQL和Statement.Vals两个参数,分别是sql语句和sql语句的入参,这两个作为入参来调用原生database/sql。我们来看下Build(...)函数。

go 复制代码
func (stmt *Statement) Build(clauses ...string) {
	var firstClauseWritten bool
	
	// 通过BuildClauses关键字出现的先后顺序构造sql 
	for _, name := range clauses {
	//获取不同DML构造结构体
		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 {
				// 调用相应dml的构造方法,构造sql,Statement.sql 采用strings.Builder函数来不断累加,当碰到语句中的占位符或者限制条件等时,
				// 就从map[string]clause.Clause 的value中取出值赋值给 vals。 这里是各个查询关键字实现累计sql的地方,不在详细说明,感兴趣的可以扒扒源码。
				c.Build(stmt)
			}
		}
	}
}

最后的执行流程应该是这样的。

这里就是gorm思想的核心了,先根据各个dml 操作的gorm链式语句,生成map[string]clause.Clause ,然后根据关键字的先后顺序BuildClauses数组,逐渐补充完整完整 Statement的SQL和Vals两个属性。gorm dml操作核心就是根据散装的map[string]clause.Clause生成Statement的SQL和Vals两个属性值 。这两个值就是调用原生database/sql的入参。

接下来就是对原生sql的调用了。比较简单我们来梳理下。

回调函数调用原生database/sql 方法

其实在讲解sql语句的组合的时候已经说过了部分调用链,我们来看下完整的调用链:

我们通过代码再来走一遍上面的流程

First(...)函数的源码如下:

go 复制代码
func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB) {

	// 由于是 First 所以返回按照主键排序的第一条 这边也加入到 sql组合数组中 链式累加
 	tx = db.Limit(1).Order(clause.OrderByColumn{
		Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey},
	})
	if len(conds) > 0 {
		if exprs := tx.Statement.BuildCondition(conds[0], conds[1:]...); len(exprs) > 0 {
			tx.Statement.AddClause(clause.Where{Exprs: exprs})
		}
	}
	tx.Statement.RaiseErrorOnNotFound = true
	tx.Statement.Dest = dest

	// 开始执行Query的回调函数,然后执行Execute(...)函数,回调函数在这个函数里进行链式调用。(dml的其他操作也是这个调用逻辑)
	return tx.callbacks.Query().Execute(tx)

其中 Query就是返回承载有回调函数的processor结构体。Query代码如下:

go 复制代码
func (cs *callbacks) Query() *processor {
	return cs.processors["query"]
}

各个dml操作返回各自的回调函数。其结构之间的关系可以看 初始化那章结构体关系图。

然后执行 processor 的Execute(...)函数 我们看下其源码:

go 复制代码
func (p *processor) Execute(db *DB) *DB {
	// ...
	// call scopes
	// sql语句拼接在f(db)中完成 然后调用database/sql 执行查询
	// 这边调用的都是注册的Query相关的函数 ,主要是查询操作,包括Query、Preload等(其他的dml操作这边是注册的相应的操作函数)
	
	for _, f := range p.fns {
		f(db)
	}

	// ...
	return db
}

Execute(...)执行链式函数到注册的Query(),其源码如下:

go 复制代码
func Query(db *gorm.DB) {
	if db.Error == nil {
		// 此函数会组装sql 和 提取出 占位符 作为 入参 调用database/sql的原生函数
		BuildQuerySQL(db)

		if !db.DryRun && db.Error == nil {
			// 调用database/sql 方法
			rows, err := db.Statement.ConnPool.QueryContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...)
			if err != nil {
				db.AddError(err)
				return
			}
			defer func() {
				db.AddError(rows.Close())
			}()
			gorm.Scan(rows, db, 0)
		}
	}
}

到这里整个调用联调就完成了,再次强调其他的dml操作的整个调用逻辑跟查询操作是一致的,只是有些调用细节不同,不在赘述。

事务

未完待续...

总结

相关推荐
Algorithm15766 分钟前
JVM是什么,与Java的关系是什么,以及JVM怎么实现的跨平台性
java·开发语言·jvm
Gnevergiveup6 分钟前
2024网鼎杯青龙组Web+Misc部分WP
开发语言·前端·python
边疆.20 分钟前
C++类和对象 (中)
c语言·开发语言·c++·算法
yy_xzz23 分钟前
QT编译报错:-1: error: cannot find -lGL
开发语言·qt
你不讲 wood25 分钟前
使用 Axios 上传大文件分片上传
开发语言·前端·javascript·node.js·html·html5
林浔090633 分钟前
C语言部分输入输出(printf函数与scanf函数,getchar与putchar详解,使用Linux ubuntu)
c语言·开发语言
一颗甜苞谷1 小时前
开源一款基于 JAVA 的仓库管理系统,支持三方物流和厂内物流,包含 PDA 和 WEB 端的源码
java·开发语言·开源
CLCNboss1 小时前
Mac安装Ruby
开发语言·经验分享·笔记·macos·ruby
ai产品老杨1 小时前
深度学习模型量化原理
开发语言·人工智能·python·深度学习·安全·音视频
秋恬意1 小时前
LinkedList 源码分析
java·开发语言·面试