增删改查(dml操作)
查询操作
gorm查询主要执行了三种操作:
- 通过链式函数调用累计查询条件(在map[string]clause.Clause中累计)
- 将查询条件转换成sql(赋值给 Statement.SQL和Statement.Vals)
- 执行对应回调函数
我们以下面简单例子来进行说明:
func TestGorm(t *testing.T) {//gormdsn := "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”),我们来看下 源码:
// 指定需要操作的表 这里会复制一份 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 = niltx.Statement.Table = ""}return
}
其中 db.getInstance() 函数控制db实例的获取方式,如下:
// 获取一个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 statementtx.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 statementtx.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”)
我们看下源码:
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是一个接口,结构体如下:
type Interface interface {Name() string // 获取关键字名字 用来生成keyBuild(Builder) // todo MergeClause(*Clause) // 添加key对应的value(sql语句)
}
几乎所有dml关键字都实现了这个接口,我们来看下有哪些,如下图:
我们来看下Where的实现逻辑,源码如下(其他的dml操作可自行查看):
func (stmt *Statement) AddClause(v clause.Interface) {if optimizer, ok := v.(StatementModifier); ok {optimizer.ModifyStatement(stmt)} else {name := v.Name() // 获取关键字 wHEREc := stmt.Clauses[name] // 获取对应的结构体c.Name = name // 赋值v.MergeClause(&c) // 添加 本sql结构体 (v中包含sql)到对应v中的数组中stmt.Clauses[name] = c // 赋值map}
}
其中v.MergeClause(&c)代码如下:
// MergeClause merge where clauses
func (where Where) MergeClause(clause *Clause) {if w, ok := clause.Expression.(Where); ok {// 深copyexprs := 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(…)源码:
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(…)函数。
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 = trueif 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(…)函数的源码如下:
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 = truetx.Statement.Dest = dest// 开始执行Query的回调函数,然后执行Execute(...)函数,回调函数在这个函数里进行链式调用。(dml的其他操作也是这个调用逻辑)return tx.callbacks.Query().Execute(tx)
其中 Query就是返回承载有回调函数的processor结构体。Query代码如下:
func (cs *callbacks) Query() *processor {return cs.processors["query"]
}
各个dml操作返回各自的回调函数。其结构之间的关系可以看 初始化那章结构体关系图。
然后执行 processor 的Execute(…)函数 我们看下其源码:
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(),其源码如下:
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操作的整个调用逻辑跟查询操作是一致的,只是有些调用细节不同,不在赘述。
事务
未完待续…