43

gorm源码解读

 4 years ago
source link: https://www.tuicool.com/articles/yiAbqqv
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

阅读高质量的开源项目,是提升代码质量的好方法。我就是抱着这种想法,提升自己的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读取步骤源码解读:数据库连接

uuEZBnE.jpg!web 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

nQFFb2f.jpg!web 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方法,注入进来啦!

ZVfqyum.jpg!web 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语句:

uqMFZzv.jpg!web debug窗口查看sql和andConditions值

6. gorm重要struct

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

  1. Callback
  2. DB
  3. Scope

函数方面:

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK