美文网首页
gorm 的 字段 clone 的设计

gorm 的 字段 clone 的设计

作者: 五岁小孩 | 来源:发表于2024-03-21 20:35 被阅读0次

gorm源码剖析 - Jxy 博客

总结

clone值 描述 设置
1 创建 DB 的默认值,表示 全新 DB,但是复用 db.Session如果NewDb=true时,则clone=1
2 则只复用 配置和Statement db.Session如果NewDb=false时,则clone=2
0 则直接复用原 db db.Where 等链式调用
  • clone = 1

    表示全新的db句柄;open创建时,clone默认=1,db.Session如果NewDb=true时,clone=1

  • clone = 0

    表示db的statement将会持续复用,调用db=db.Where(),db.clone=0

  • clone = 2

    则创建新的 db,并且复用之前旧 db 的 配置和Statement

clone的设计目的:并发安全,我们都知道解决并发安全方法有:加锁解决,避免数据冲突;

gorm通过设计clone的值来避免并发操作db,如在构建where条件时,内部通过调用db.getInstance()返回db的副本,从而避免并发操作

clone 的设计

clone=1

表示一个全新配置、全新statement(sql条件)的db

func main(){
  // 创建一个db句柄,此时db.clone=1
  db, err := gorm.Open(mysql.New(mysql.Config{
            }), &gorm.Config{})

}
func Open(dialector Dialector, opts ...Option) (db *DB, err error) {
    config := &Config{}
    .............

    db = &DB{Config: config, clone: 1}
  
    .............

    return
}

clone=0

表示db的statement将会持续复用

func main(){
  db,_:=gorm.Open(...) // db.clone = 1
  
  db.Where(...)  // 这里db的statement条件时不会生效的,db.clone=1 
  
  db=db.Where(...) // db.clone = 0, 内部调用了db.getInstance(),可以理解为返回了db.statement的副本,并发安全
  
}

调用db.Table,Where,Or,Not等链式操作方法,都会调用此方法

// 传入的db的clone:
//  1.clone为0:
//      则返回原先的db,后续的db操作会影响原先的db,clone为原先的clone的值为0
//  2.clone为1:
//      返回一个带有db.Config的新db,且Statement为全新创建的,且clone值为0,后续的操作不会影响原先的db
//  3.clone为2:
//      返回一个带有db.Config的新db,且Statement为原先db的Statement,且clone值为0,后续的操作不会影响原先的db

func (db *DB) getInstance() *DB {
    if db.clone > 0 { // clone=1 or clone=2,修改db的一些参数,然后返回副本
        tx := &DB{Config: db.Config, Error: db.Error} // 复用db的配置创建新的db,db.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{},
                Vars:     make([]interface{}, 0, 8),
            }
        } else {
      // db.clone=2,则复杂db的statement
            // with clone statement
            tx.Statement = db.Statement.clone()
            tx.Statement.DB = tx
        }
        return tx
    }
  // db.clone = 0,则直接返回当前db
    return db
}
// 代码简化
func (db *DB) getInstance() *DB {
    switch db.clone:
    case 0:
        return db
    case 1:
        return newStatement() // 一个全新的,空白的Statement
    case 2:
        return db.cloneStatement() // 将之前的Statement复制一份
}

clone=2

则创建新的 db,并且复用之前旧 db 的 配置和Statement

func main(){
  db,_:=gorm.Open(...) // db.clone = 1
  
  db.Transaction(func(tx *gorm.DB) error {} // 内部调用 fc(db.Session(&Session{})) 返回db且db.clone=2
                 
  db.Session(&gorm.Session{NewDB: true, Context: context.Background()}) // 带NewDB: true,则返回的db为全新的db,全新的statement的
}

工具


// CloneDB 只会复制 db.Statement
func CloneDB(db *gorm.DB) *gorm.DB {
    // return db.WithContext(db.Statement.Context)
    return db.WithContext(context.Background())
}

// NewDB 完全新创建一个 DB
func NewDB(db *gorm.DB) *gorm.DB {
    return db.Session(&gorm.Session{NewDB: true, Context: context.Background()})
    // return db.Session(&gorm.Session{NewDB: true, Context: db.Statement.Context})
}

// CleanDB 使用原有 context 创建一个新的 DB
func CleanDB(db *gorm.DB) *gorm.DB {
    // return db.Session(&gorm.Session{NewDB: true, Context: context.Background()})
}

疑问

  • gorm创建的全局句柄在使用过程中为什么不会出现statement(where等)混乱
// 创建全局DB句柄,此时的clone=1
var db=dao.NewDevInventoryDBHelper().DB
func TestGorm(t *testing.T)  {
    db.Where("name=?","xj") //clone=1会返回的新的db,如果不用新的变量接受,该where不做落在原的db上,
    db=db.Where("name=?","xj") // clone=1,会返回一个新的db,但是会覆盖原先全局的db,导致后续的db操作都会带上此where的条件
    db2:=db.Where("name=?","xj") // 该操作合理,返回新的db的clone=0,后续的操作可以不需要用变量再接收,也不会影响全局的db
    db2.Where("name=?","xj")
}
## Where 内部会调用db.getInstance()
## 此时的db的clone为1
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})

// 安全的使用新初始化的 *gorm.DB
for i := 0; i < 100; i++ {
  ## 每次返回的db都是clone为0的新db,所以是安全的
  go db.Where(...).First(&user)
}
## 此tx的clone=0
tx := db.Where("name = ?", "jinzhu")
// 不安全的复用 Statement
for i := 0; i < 100; i++ {
  ## 此处的tx的clone为0,调用where之后会复用tx,导致所有的where都坐落在同一个tx上
  go tx.Where(...).First(&user)
}

ctx, _ := context.WithTimeout(context.Background(), time.Second)
## db.Session(&Session{Context: ctx}) 将clone重置为2
ctxDB := db.WithContext(ctx)
// 在 `新建会话方法` 之后是安全的
for i := 0; i < 100; i++ {
  ## 每次返回的db都是clone为0的新db,所以是安全的
  go ctxDB.Where(...).First(&user)
}

ctx, _ := context.WithTimeout(context.Background(), time.Second)
ctxDB := db.Where("name = ?", "jinzhu").WithContext(ctx)
// 在 `新建会话方法` 之后是安全的
for i := 0; i < 100; i++ {
  go ctxDB.Where(...).First(&user) // `name = 'jinzhu'` 会应用到查询中
}

tx := db.Where("name = ?", "jinzhu").Session(&gorm.Session{})
// 在 `新建会话方法` 之后是安全的
for i := 0; i < 100; i++ {
  go tx.Where(...).First(&user) // `name = 'jinzhu'` 会应用到查询中
}
有劳各位看官 点赞、关注➕收藏,你们的支持是我最大的动力!!!
接下来会不断更新 golang 的一些底层源码及个人开发经验(个人见解)!!!
同时也欢迎大家在评论区提问、分享您的经验和见解!!!

下一期:【 Go 源码之 Map】

相关文章

网友评论

      本文标题:gorm 的 字段 clone 的设计

      本文链接:https://www.haomeiwen.com/subject/mjqqtjtx.html