GORM源码阅读与分析

最近读了一下GORM的源码,以下是简述。我们将以一个查询来看GORM是怎么调用的。

事前准备

新建一个数据库,建一张表,并且插入一行数据:

mysql [email protected]:(none)> create database hello default charset utf8mb4;
Query OK, 1 row affected
Time: 0.002s
mysql [email protected]:(none)> use hello
You are now connected to database "hello" as user "root"
Time: 0.001s
mysql [email protected]:hello> CREATE TABLE `users` (
                                `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
                                `created_at` timestamp NULL DEFAULT NULL,
                                `updated_at` timestamp NULL DEFAULT NULL,
                                `deleted_at` timestamp NULL DEFAULT NULL,
                                `name` varchar(255) DEFAULT NULL,
                                PRIMARY KEY (`id`),
                                KEY `idx_resources_deleted_at` (`deleted_at`)
                            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Query OK, 0 rows affected
Time: 0.020s
mysql [email protected]:hello> show tables;
+-----------------+
| Tables_in_hello |
+-----------------+
| users           |
+-----------------+
1 row in set
Time: 0.013s
mysql [email protected]:hello> insert into users(id, name) values (1, 'me');
Query OK, 1 row affected
Time: 0.004s
mysql [email protected]:hello> select * from users;
+----+------------+------------+------------+------+
| id | created_at | updated_at | deleted_at | name |
+----+------------+------------+------------+------+
| 1  | <null>     | <null>     | <null>     | me   |
+----+------------+------------+------------+------+
1 row in set
Time: 0.016s

先看 database/sql

GORM本身是构建在 database/sql 之上的。我们先看看典型的 database/sql 应该咋用:

package main

import (
	"database/sql"
	"log"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	db, err := sql.Open("mysql", "root:1234567@tcp(127.0.0.1:3306)/hello")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	var (
		id   int
		name string
	)
	rows, err := db.Query("select id, name from users where id = ?", 1)
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()

	for rows.Next() {
		err := rows.Scan(&id, &name)
		if err != nil {
			log.Fatal(err)
		}
		log.Println(id, name)
	}

	err = rows.Err()
	if err != nil {
		log.Fatal(err)
	}
}

看得出来,关键就在于,第一,db.Query,第二,rows.Scan,而刚好GORM是构建在 database/sql 之上的,所以GORM要做的事情 就是:

  • 通过struct tags来生成数据库model
  • 当查询时,通过反射把数据查询到对应的此前定义的model

查看GORM代码

我们看看上面的例子,GORM该怎么写:

package main

import (
	"log"

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

type User struct {
	gorm.Model
	Name string
}

func main() {
	db, err := gorm.Open("mysql", "root:1234567@tcp(127.0.0.1:3306)/hello")
	if err != nil {
		panic("failed to connect database")
	}
	defer db.Close()

	var user User
	err = db.Where("id = ?", 1).First(&user).Error
	log.Printf("user: %+v, error: %s", user, err)
}

可以看出来,GORM使用了链式API。

GORM有三个重要的组成结构:

  • gorm.DB 用来保存当前DB连接的各种信息

    // DB contains information for current db connection
    type DB struct {
        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
    }
    
  • gorm.search 用来保存各种查询条件

    type search struct {
        db               *DB
        whereConditions  []map[string]interface{}
        orConditions     []map[string]interface{}
        notConditions    []map[string]interface{}
        havingConditions []map[string]interface{}
        joinConditions   []map[string]interface{}
        initAttrs        []interface{}
        assignAttrs      []interface{}
        selects          map[string]interface{}
        omits            []string
        orders           []interface{}
        preload          []searchPreload
        offset           interface{}
        limit            interface{}
        group            string
        tableName        string
        raw              bool
        Unscoped         bool
        ignoreOrderQuery bool
    }
    
  • gorm.Scope 用来保存当前操作的各种信息

    // Scope contain current operation's information when you perform any operation on the database
    type Scope struct {
        Search          *search
        Value           interface{}
        SQL             string
        SQLVars         []interface{}
        db              *DB
        instanceID      string
        primaryKeyField *Field
        skipLeft        bool
        fields          *[]*Field
        selectAttrs     *[]string
    }
    

我们来看看上面的 db.Where("id = ?", 1).First(&user) 的代码调用:

  • db.Where("id = ?", 1)这里,就是调用这段代码:

    func (s *DB) Where(query interface{}, args ...interface{}) *DB {
    	return s.clone().search.Where(query, args...).db
    }
    
    func (s *DB) clone() *DB {
    	db := &DB{
    		db:                s.db,
    		parent:            s.parent,
    		logger:            s.logger,
    		logMode:           s.logMode,
    		values:            map[string]interface{}{},
    		Value:             s.Value,
    		Error:             s.Error,
    		blockGlobalUpdate: s.blockGlobalUpdate,
    	}
    
    	for key, value := range s.values {
    		db.values[key] = value
    	}
    
    	if s.search == nil {
    		db.search = &search{limit: -1, offset: -1}
    	} else {
    		db.search = s.search.clone()
    	}
    
    	db.search.db = db
    	return db
    }
    
    func (s *search) Where(query interface{}, values ...interface{}) *search {
    	s.whereConditions = append(s.whereConditions, map[string]interface{}{"query": query, "args": values})
    	return s
    }
    

可以看到,其实就是增加了查询条件。

  • .First(&user) 就是:

    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
    }
    
    // queryCallback used to query data from database
    func queryCallback(scope *Scope) {
    // ... 省略
    
    	scope.prepareQuerySQL()
    
    	if !scope.HasError() {
    		scope.db.RowsAffected = 0
    		if str, ok := scope.Get("gorm:query_option"); ok {
    			scope.SQL += addExtraSpaceIfExist(fmt.Sprint(str))
    		}
    
    		if rows, err := scope.SQLDB().Query(scope.SQL, scope.SQLVars...); scope.Err(err) == nil {
    			defer rows.Close()
    
    			columns, _ := rows.Columns()
    			for rows.Next() {
    				scope.db.RowsAffected++
    
    				elem := results
    				if isSlice {
    					elem = reflect.New(resultType).Elem()
    				}
    
    				scope.scan(rows, columns, scope.New(elem.Addr().Interface()).Fields())
    
    				if isSlice {
    					if isPtr {
    						results.Set(reflect.Append(results, elem.Addr()))
    					} else {
    						results.Set(reflect.Append(results, elem))
    					}
    				}
    			}
    
    			if err := rows.Err(); err != nil {
    				scope.Err(err)
    			} else if scope.db.RowsAffected == 0 && !isSlice {
    				scope.Err(ErrRecordNotFound)
    			}
    		}
    	}
    }
    

可以看到,就是最后调用了 DefaultCallback 里的 queryCallback,而 queryCallback 其实就是我们最上面说的,GORM该做的事情。 并且可以看出来,执行 .First() 的时候,新建了一个 Scope,这也符合scope的定义。



更多文章
  • 善用闭包(closure)让Go代码更优雅
  • Golang的可选参数实践
  • FreeBSD ipfw使用教程
  • Golang expvar库源码阅读
  • Golang SQL生成库 Squirrel 教程及源码阅读
  • Golang validator使用教程
  • 使用Redis的Stream模块实现群聊功能
  • 价值编程与职业发展
  • 解决k8s cron无法读取环境变量的问题
  • 应用内购的坑
  • 两种常见的访问控制模型
  • gunicorn max-requests 选项的作用
  • Redis使用中的几点注意事项
  • 给你的代码跑个分?pylint使用教程
  • 一个Gunicorn worker数量引发的血案