阅读高质量的开源项目,是提升代码质量的好方法。我就是抱着这种想法,提升自己的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函数