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:[email protected](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:[email protected](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的定义。


  • https://golang.org/pkg/database/sql/
  • http://doc.gorm.io/development.html#architecture

更多文章
  • Docker CE 18.03源码阅读与分析
  • 容器时代的日志处理
  • Golang和Thrift
  • 折腾Kubernetes
  • 协程(coroutine)简介 - 什么是协程?
  • goroutine 切换的时候发生了什么?
  • Prometheus 数据类型
  • Gin源码阅读与分析
  • 如何面试-作为面试官得到的经验
  • 自己写一个容器