Gorm 官网文档提供了如何正确应用链式调用的例子以及会引起协程不平安的反例,晓得了如何正确应用,也要晓得原理能力用的更安心。上面以文档示例和源码切入,浅析 Gorm 在链式调用时时如何保障协程平安的?

源码剖析

上面是一段 gorm 常见的应用代码,先初始化连贯,而后依据链式调用进行增删改。

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {    panic("failed to connect database")}tx := db.Where("age = 22").Where("name = '小明'").Find(&user)

通过 gorm.Open() 初始化连贯,将拿到一个 *gorm.DB 构造体指针 db。构造体的 clone 属性为 1

type DB struct {    *Config    Error        error    RowsAffected int64    Statement    *Statement    clone        int}func Open(dialector Dialector, opts ...Option) (db *DB, err error) {    // ……    db = &DB{Config: config, clone: 1}    // ……}

通过 db.Where() 进行链式查问,Where() 办法也将返回一个*gorm.DB构造体指针 tx。

// Where add conditionsfunc (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()、Select()、Limit() 等每个链式办法中,都要先去获取 *gorm.DB构造体指针 tx,也就是 tx = db.getInstance()

func (db *DB) getInstance() *DB {    if db.clone > 0 {        tx := &DB{Config: db.Config, Error: db.Error}        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 {            // with clone statement            tx.Statement = db.Statement.clone()            tx.Statement.DB = tx        }        return tx    }    return db}

当传入的 *gorm.DB指针指向的构造体属性 clone 为 1时,将会克隆一份 DB 构造体,并返回一个新的指针指向这个 DB 构造体,也即是创立一个新的会话。clone 为 0 时,返回原来的指针指向地址。

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {    panic("failed to connect database")}db.Where().Where()// 等同于tx1 = db.Where()tx2 = tx.Where()

基于下面的剖析,在此例代码中,因为 db 指针指向 gorm.DB 构造体的 clone 属性为1。tx1 将指向一个复制的新的 gorm.DB 构造体,它的 clone 属性为 0。所以,tx.Where() 时将不会复制一个新的构造体(不会创立新会话),即 tx2 与 tx1 都指向同一个 gorm.DB 构造体,与 db 指向不同。

例子

通过下面的分析,再看官网文档的例子,就能了解怎么应用是协程平安的链式调用。

例子1

db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})// 平安的应用新初始化的 *gorm.DBfor i := 0; i < 100; i++ {  go db.Where(...).First(&user)}

在 100 个协程中,db.Where() 会别离复制 gorm.DB 构造体,返回其指向指针,即创立了新会话,而后持续进行链式调用。所以,在 100 个协程中,链式调用拼接的 sql 查问是不会互相烦扰的。

例子2

tx := db.Where("name = ?", "jinzhu")// 不平安的复用 Statementfor i := 0; i < 100; i++ {  go tx.Where(...).First(&user)}

本例中,tx 指向了一个新的 gorm.DB 构造体,且 clone 为 0,所以 tx. Where() 将不会产生新的构造体,即不会创立新会话。那么,在100个协程中,共用 tx 指向的 gorm.BD,这样就会产生协程间互相烦扰的问题。

例子3

tx := db.Where("name = ?", "jinzhu").Session(&gorm.Session{})// 在 `新建会话办法` 之后是平安的for i := 0; i < 100; i++ {  go tx.Where(...).First(&user) // `name = 'jinzhu'` 会利用到查问中}

通过 Session() 办法,创立新会话,将 tx 指向的新构造体 的 clone 属性置为 1。“tx 便有了例子一中 db 的成果”。

例子4

ctx, _ := context.WithTimeout(context.Background(), time.Second)ctxDB := db.WithContext(ctx)// 在 `新建会话办法` 之后是平安的for i := 0; i < 100; i++ {  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'` 会利用到查问中}

这两种形式是复用了 Session 办法。

finish!

参考
链式办法 - GORM

文章来自 gorm是如何保障协程平安的