gorm源码解读
阅读高质量的开源项目,是提升代码质量的好方法。我就是抱着这种想法,提升自己的golang编程技能。代码库,我选择了2个:
- 应用场景非常熟悉的orm:gorm,毕竟之前写java,用了不少hibernate
- 场景还在学习中的区块链:以太坊的go-ethereum
最近阅读go的orm框架gorm,收获颇多。项目作者是中国人,github点赞数量9977(截止2018/8/20日)。
gorm名字起的很霸气,应该是:go+orm—>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 }
以上代码,做了这些事情:
- 连接mysql数据库
- 读取Product表,将id=1的第一条记录(应该只有1条或者0条记录)读取到product变量里面,供业务程序使用
3. gorm读取步骤源码解读:数据库连接
db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")
- 打开数据库连接(ps:此处叫连接可能有点不合适,源代码注释是:Open initialize a new db connection, need to import driver first)
- 设置db.parent为当前db
- 设置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个重要代码:
- inlineCondition(where)
- callCallbacks(s.parent.callbacks.queries)
4. gorm读取步骤源码解读:callCallbacks
callCallback是逐步对多个Callback发起call,也就是按顺序调用callbacks。每个Callback做一件事情,比如读取数据库值mapping到struct,级联读取其他值。这样好处是:
- callback设计比较简单,做一件事(看下面源码,就指定其实是就是具有相同签名的函数)
- 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方法,注入进来啦!
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语句:
6. gorm重要struct
个人觉得弄懂以下几个struct,gorm设计思路就弄懂一半了。
- Callback
- DB
- Scope
函数方面:
- callback_*.go文件注册各个callback函数