gorm源码解读

阅读高质量的开源项目,是提升代码质量的好方法。我就是抱着这种想法,提升自己的golang编程技能。代码库,我选择了2个:

  1. 应用场景非常熟悉的orm:gorm,毕竟之前写java,用了不少hibernate
  2. 场景还在学习中的区块链:以太坊的go-ethereum

最近阅读go的orm框架gorm,收获颇多。项目作者是中国人,github点赞数量9977(截止2018/8/20日)。

gorm名字起的很霸气,应该是:go+orm—>gorm这个意思!

  1. gorm源码
  2. gorm官网

1. go原始的sql操作

go原始sql操作比较简单,就是将数据读写到一个变量或者多个变量里面。

// from: https://golang.org/src/database/sql/example_test.go
var sql string = "..."
rows, err := db.Query(sql, age)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()


for rows.Next() {
    var (
        id   int64
        name string
    )
    if err := rows.Scan(&id, &name); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("id %d name is %s\n", id, name)
}

使用struct也是可以的:

type struct TestStruct {
    id int64
    name string
}

var sql string = "..."
rows, err := db.Query(sql, age)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()


for rows.Next() {
    var record TestStruct{}
    if err := rows.Scan(&record.id, &record.name); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("id %d name is %s\n", id, name)
}

这种原始操作,缺少object mapping思想。必须要写sql,要将查询出来的字段映射(mapping)到变量或者struct,极大降低了编程效率。

2. gorm的读取demo

先看代码:

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

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


func main() {
  db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")
  defer db.Close()

  var product Product
  db.First(&product, 1) // find product with id 1
}

以上代码,做了这些事情:

  1. 连接mysql数据库
  2. 读取Product表,将id=1的第一条记录(应该只有1条或者0条记录)读取到product变量里面,供业务程序使用

3. gorm读取步骤源码解读:数据库连接

gorm数据库连接框架
db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")
  1. 打开数据库连接(ps:此处叫连接可能有点不合适,源代码注释是:Open initialize a new db connection, need to import driver first)
  2. 设置db.parent为当前db
  3. 设置SQLCommon(会委派给go自带db)

此处列出gorm的相关代码:

db = &DB{
        db:        dbSQL,
        logger:    defaultLogger,
        values:    map[string]interface{}{},
        callbacks: DefaultCallback,
        dialect:   newDialect(dialect, dbSQL),
    }
    db.parent = db

查找数据库操作First,按照指定条件查询,返回结果按照主键升序排列,最多只返回一条记录。

gorm的First源码是:

// First find first record that match given conditions, order by primary key
func (s *DB) First(out interface{}, where ...interface{}) *DB {
    newScope := s.NewScope(out)
    newScope.Search.Limit(1)
    return newScope.Set("gorm:order_by_primary_key", "ASC").
        inlineCondition(where...)
        .callCallbacks(s.parent.callbacks.queries).db
}

来阅读下s.NewScope吧。

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

克隆了一个db实例,设置输出值,相当于开辟了一个干净的数据库“交互环境”。就因为这里的克隆操作,前面我提到db的意思描述成数据库连接,并不合适。

这个克隆的db实例,包裹在Scope里面。在刚才First方法里面,也就是First方法内有效。所以,业务代码持有的总是最原始的db实例,即通过gorm.Open出来的db实例。

假如,业务代码继续其他db操作。gorm的其他方法(如Find/First/Update等)都会再克隆一个db,“包裹”在scope里面,进行操作。

剩下有2个重要代码:

  1. inlineCondition(where)
  2. callCallbacks(s.parent.callbacks.queries)

4. gorm读取步骤源码解读:callCallbacks

gorm 的 callback设计

callCallback是逐步对多个Callback发起call,也就是按顺序调用callbacks。每个Callback做一件事情,比如读取数据库值mapping到struct,级联读取其他值。这样好处是:

  1. callback设计比较简单,做一件事(看下面源码,就指定其实是就是具有相同签名的函数)
  2. callbacks拓展性好,即s.parent.callbacks.queries, s.parent.callbacks.queries, s.parent.callbacks.deletes等执行过程可随意扩展

Callback struct源码:

// Callback is a struct that contains all CRUD callbacks
//   Field `creates` contains callbacks will be call when creating object
//   Field `updates` contains callbacks will be call when updating object
//   Field `deletes` contains callbacks will be call when deleting object
//   Field `queries` contains callbacks will be call when querying object with query methods like Find, First, Related, Association...
//   Field `rowQueries` contains callbacks will be call when querying object with Row, Rows...
//   Field `processors` contains all callback processors, will be used to generate above callbacks in order
type Callback struct {
    creates    []*func(scope *Scope) 
    updates    []*func(scope *Scope)
    deletes    []*func(scope *Scope)
    queries    []*func(scope *Scope)
    rowQueries []*func(scope *Scope)
    processors []*CallbackProcessor
}

这些Callback什么时候被初始化,并设置值了呢?在各个callback_*.go的init方法,注入进来啦!

init方法注入 callback

5. gorm读取步骤源码解读:inlineCondition(where)

业务代码:

db.First(&product, 1) // find product with id 1

相当于设置了id=1的查询条件。inlineCondition将条件值1(即data=1)设置为where条件。

奥秘在:scope.whereSQL方法里面。

for _, clause := range scope.Search.whereConditions {
        if sql := scope.buildCondition(clause, true); sql != "" {
            andConditions = append(andConditions, sql)
        }
    }

scope的buildCondition方法,将条件值1(即data=1),转换成sql语句:

switch value := clause["query"].(type) {
    case sql.NullInt64:
        return fmt.Sprintf("(%v.%v %s %v)", quotedTableName, quotedPrimaryKey, equalSQL, value.Int64)
    case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
        return fmt.Sprintf("(%v.%v %s %v)", quotedTableName, quotedPrimaryKey, equalSQL, value)

使用debug追逐,可以查看到sql语句:

debug窗口查看sql和andConditions值

6. gorm重要struct

个人觉得弄懂以下几个struct,gorm设计思路就弄懂一半了。

  1. Callback
  2. DB
  3. Scope

函数方面:

  1. callback_*.go文件注册各个callback函数