01GORM源码解读

简介

GORM 源码解读, 基于 v1.9.11 版本.

起步

官方文档上入门的例子如下:

package main

import (
  "github.com/jinzhu/gorm"
  _ "github.com/jinzhu/gorm/dialects/sqlite"
)

type Product struct {
  gorm.Model
  Code string
  Price uint
}

func main() {
  db, err := gorm.Open("sqlite3", "test.db")
  if err != nil {
    panic("failed to connect database")
  }
  defer db.Close()

  // Migrate the schema
  db.AutoMigrate(&Product{})

  // 创建
  db.Create(&Product{Code: "L1212", Price: 1000})

  // 读取
  var product Product
  db.First(&product, 1) // 查询id为1的product
  db.First(&product, "code = ?", "L1212") // 查询code为l1212的product

  // 更新 - 更新product的price为2000
  db.Model(&product).Update("Price", 2000)

  // 删除 - 删除product
  db.Delete(&product)
}

数据库连接

gorm.Open 开始看起吧, 看数据库是怎么连接的:

// Open initialize a new db connection, need to import driver first, e.g:
//
//     import _ "github.com/go-sql-driver/mysql"
//     func main() {
//       db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")
//     }
// GORM has wrapped some drivers, for easier to remember driver's import path, so you could import the mysql driver with
//    import _ "github.com/jinzhu/gorm/dialects/mysql"
//    // import _ "github.com/jinzhu/gorm/dialects/postgres"
//    // import _ "github.com/jinzhu/gorm/dialects/sqlite"
//    // import _ "github.com/jinzhu/gorm/dialects/mssql"
func Open(dialect string, args ...interface{}) (db *DB, err error) {
    if len(args) == 0 {
        err = errors.New("invalid database source")
        return nil, err
    }
    var source string
    var dbSQL SQLCommon
    var ownDbSQL bool

    switch value := args[0].(type) {
    case string:
        var driver = dialect
        if len(args) == 1 {
            source = value
        } else if len(args) >= 2 {
            driver = value
            source = args[1].(string)
        }
        dbSQL, err = sql.Open(driver, source)
        ownDbSQL = true
    case SQLCommon:
        dbSQL = value
        ownDbSQL = false
    default:
        return nil, fmt.Errorf("invalid database source: %v is not a valid type", value)
    }

    db = &DB{
        db:        dbSQL,
        logger:    defaultLogger,
        callbacks: DefaultCallback,
        dialect:   newDialect(dialect, dbSQL),
    }
    db.parent = db
    if err != nil {
        return
    }
    // Send a ping to make sure the database connection is alive.
    if d, ok := dbSQL.(*sql.DB); ok {
        if err = d.Ping(); err != nil && ownDbSQL {
            d.Close()
        }
    }
    return
}

gorm.Open 有两个参数, 一个是数据库名称, 其余是连接参数.

switch 语句中, 可以发现如果第一个参数是 string 类型, 实际上是通过 Golang 中的 sql 模块连接:

dbSQL, err = sql.Open(driver, source)

也可以直接传递一个实现了 SQLCommon 接口的实例.

然后初始化了一个 gorm.DB 实例, 并在最后执行了一次 ping 请求, 测试数据库连接是否正常.

看一下 gorm.DB 结构体:

// DB contains information for current db connection
type DB struct {
    sync.RWMutex
    Value        interface{}
    Error        error
    RowsAffected int64

    // single db
    db                SQLCommon
    blockGlobalUpdate bool
    logMode           logModeValue
    logger            logger
    search            *search
    values            sync.Map

    // global db
    parent        *DB
    callbacks     *Callback
    dialect       Dialect
    singularTable bool

    // function to be used to override the creating of a new timestamp
    nowFuncOverride func() time.Time
}

gorm.DB 扩展自 sync.RWMutex 读写互斥锁.

gorm.DB

上面已经看过了 gorm.DB 结构体的定义了, 从入门的示例代码中可以看出, 所有的操作都是围绕它来进行的,
所以 gorm.DB 是核心的结构体. 看下它具体实现了哪些方法.

// New clone a new db connection without search conditions
func (s *DB) New() *DB {
    clone := s.clone()
    clone.search = nil
    clone.Value = nil
    return clone
}

type closer interface {
    Close() error
}

// Close close current db connection.  If database connection is not an io.Closer, returns an error.
func (s *DB) Close() error {
    if db, ok := s.parent.db.(closer); ok {
        return db.Close()
    }
    return errors.New("can't close current db")
}

克隆数据库的连接和关闭数据库连接. New 方法内部使用到了 s.clone(),

func (s *DB) clone() *DB {
    db := &DB{
        db:                s.db,
        parent:            s.parent,
        logger:            s.logger,
        logMode:           s.logMode,
        Value:             s.Value,
        Error:             s.Error,
        blockGlobalUpdate: s.blockGlobalUpdate,
        dialect:           newDialect(s.dialect.GetName(), s.db),
        nowFuncOverride:   s.nowFuncOverride,
    }

    s.values.Range(func(k, v interface{}) bool {
        db.values.Store(k, v)
        return true
    })

    if s.search == nil {
        db.search = &search{limit: -1, offset: -1}
    } else {
        db.search = s.search.clone()
    }

    db.search.db = db
    return db
}

略过一些简单的 get/set 方法, 接着看

// NewScope create a scope for current operation
func (s *DB) NewScope(value interface{}) *Scope {
    dbClone := s.clone()
    dbClone.Value = value
    scope := &Scope{db: dbClone, Value: value}
    if s.search != nil {
        scope.Search = s.search.clone()
    } else {
        scope.Search = &search{}
    }
    return scope
}

NewScope 会为当前的操作创建一个新的 scope (作用域).

// QueryExpr returns the query as expr object
func (s *DB) QueryExpr() *expr {
    scope := s.NewScope(s.Value)
    scope.InstanceSet("skip_bindvar", true)
    scope.prepareQuerySQL()

    return Expr(scope.SQL, scope.SQLVars...)
}

// SubQuery returns the query as sub query
func (s *DB) SubQuery() *expr {
    scope := s.NewScope(s.Value)
    scope.InstanceSet("skip_bindvar", true)
    scope.prepareQuerySQL()

    return Expr(fmt.Sprintf("(%v)", scope.SQL), scope.SQLVars...)
}

QueryExprSubQuery 都用到了 NewScope, 在当前的作用域下获取查询表达式和进行子查询.

接着是很多查询方法, 类似 Where,

// Where return a new relation, filter records with given conditions, accepts `map`, `struct` or `string` as conditions, refer http://jinzhu.github.io/gorm/crud.html#query
func (s *DB) Where(query interface{}, args ...interface{}) *DB {
    return s.clone().search.Where(query, args...).db
}

跳过这些方法, 等后面探究查询表达式的时候再详细研究.

// Scopes pass current database connection to arguments `func(*DB) *DB`, which could be used to add conditions dynamically
//     func AmountGreaterThan1000(db *gorm.DB) *gorm.DB {
//         return db.Where("amount > ?", 1000)
//     }
//
//     func OrderStatus(status []string) func (db *gorm.DB) *gorm.DB {
//         return func (db *gorm.DB) *gorm.DB {
//             return db.Scopes(AmountGreaterThan1000).Where("status in (?)", status)
//         }
//     }
//
//     db.Scopes(AmountGreaterThan1000, OrderStatus([]string{"paid", "shipped"})).Find(&orders)
// Refer https://jinzhu.github.io/gorm/crud.html#scopes
func (s *DB) Scopes(funcs ...func(*DB) *DB) *DB {
    for _, f := range funcs {
        s = f(s)
    }
    return s
}

Scopes 是一个钩子函数, 用于动态添加查询条件, 这在函数是一等公民的语言里是一个常见的模式.

事务实现

看一下事务是如何实现的:

// Begin begins a transaction
func (s *DB) Begin() *DB {
    return s.BeginTx(context.Background(), &sql.TxOptions{})
}

// BeginTx begins a transaction with options
func (s *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) *DB {
    c := s.clone()
    if db, ok := c.db.(sqlDb); ok && db != nil {
        tx, err := db.BeginTx(ctx, opts)
        c.db = interface{}(tx).(SQLCommon)

        c.dialect.SetDB(c.db)
        c.AddError(err)
    } else {
        c.AddError(ErrCantStartTransaction)
    }
    return c
}

这一部分是开始事务时的操作, 实际上是 c.db 实现了 sqlDb 接口, 调用了 BeginTx 方法.

type sqlDb interface {
    Begin() (*sql.Tx, error)
    BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)
}

接着看如何提交事务:

// Commit commit a transaction
func (s *DB) Commit() *DB {
    var emptySQLTx *sql.Tx
    if db, ok := s.db.(sqlTx); ok && db != nil && db != emptySQLTx {
        s.AddError(db.Commit())
    } else {
        s.AddError(ErrInvalidTransaction)
    }
    return s
}

和开始事务类似, s.db 实现了 sqlTx 接口, 调用了 Commit 方法.

type sqlTx interface {
    Commit() error
    Rollback() error
}

sqlTx 接口里还有个 Rollback 方法, 所以回滚操作也是类似的:

// Rollback rollback a transaction
func (s *DB) Rollback() *DB {
    var emptySQLTx *sql.Tx
    if db, ok := s.db.(sqlTx); ok && db != nil && db != emptySQLTx {
        if err := db.Rollback(); err != nil && err != sql.ErrTxDone {
            s.AddError(err)
        }
    } else {
        s.AddError(ErrInvalidTransaction)
    }
    return s
}

// RollbackUnlessCommitted rollback a transaction if it has not yet been
// committed.
func (s *DB) RollbackUnlessCommitted() *DB {
    var emptySQLTx *sql.Tx
    if db, ok := s.db.(sqlTx); ok && db != nil && db != emptySQLTx {
        err := db.Rollback()
        // Ignore the error indicating that the transaction has already
        // been committed.
        if err != sql.ErrTxDone {
            s.AddError(err)
        }
    } else {
        s.AddError(ErrInvalidTransaction)
    }
    return s
}

RollbackUnlessCommittedRollback 的区别在于前者少了一个 err != nil 的判断,
看了半天还是难以理解这有什么差别.

RollbackUnlessCommitted 作者给出的例子如下:

func doTransaction(DB *gorm.DB) error {
  tx := DB.Begin()
  defer tx.RollbackUnlessCommitted()

  u := User{Name: "test"}
  if err != tx.Save(&User).Error; err != nil {
    return err
  }
  return tx.Commit().Error
}

相比较而言, 官方文档上事务的例子如下:

func CreateAnimals(db *gorm.DB) error {
  // 请注意,事务一旦开始,你就应该使用 tx 作为数据库句柄
  tx := db.Begin()
  defer func() {
    if r := recover(); r != nil {
      tx.Rollback()
    }
  }()

  if err := tx.Error; err != nil {
    return err
  }

  if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
    tx.Rollback()
    return err
  }

  if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
    tx.Rollback()
    return err
  }

  return tx.Commit().Error
}

总结

暂时就看到这里吧, gorm.DB 还有很多方法等待后续发掘.

主要看了数据库的连接过程, 基本是通过 dbSQL, err = sql.Open(driver, source) 实现的.

也看了事务部分, 主要是要实现两个接口:

type sqlDb interface {
    Begin() (*sql.Tx, error)
    BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)
}

type sqlTx interface {
    Commit() error
    Rollback() error
}

当然, 对于其中的 RollbackUnlessCommittedRollback 有点疑惑, 因为我想不明白到底有什么不同.

既然是 ORM, 模型定义应该是重中之重, 后续将探索 Model 实现.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容

  • 当夜幕降临, 远方的你,是否也会彷徨? 当夜幕降临, 天涯某处的你,是否也会迷茫? 可是, 亲爱的你呀~ 何必彷徨...
    风尖刃阅读 346评论 0 0
  • 清明节扫墓是一项历史悠久的传统习俗,也是缅怀先人的一种方式。祭祀先人是一种很肃穆很庄严的仪式。 ​扫墓要选择好时间...
    黄德有阅读 463评论 0 1
  • 今天不上学,可是上辅导班,今天就没有上学时那么急,七点半送去就好,我把孩子送去,昨晚给赵乐捎的卷子忘拿了,就又回来...
    2018级高佳浩妈妈阅读 208评论 0 3
  • 今天《老子今注今译》终于到手,遂决定如昨天所言,增加时间和生命的厚度。具体的做法是每天背一章,全部背会后期待多体悟...
    西瓜0707阅读 164评论 0 1