增删改查(dml操作)
查询操作
gorm查询主要执行了三种操作:
- 通过链式函数调用累计查询条件(在map[string]clause.Clause中累计)
- 将查询条件转换成sql(赋值给 Statement.SQL和Statement.Vals)
- 执行对应回调函数
我们以下面简单例子来进行说明:
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操作的整个调用逻辑跟查询操作是一致的,只是有些调用细节不同,不在赘述。
事务
未完待续...