Gorm 源碼分析(二) 簡單query分析

簡單使用

上一篇文章咱們已經知道了不使用orm如何調用mysql數據庫,這篇文章咱們要查看的是Gorm的源碼,從最簡單的一個查詢語句做爲切入點。固然Gorm的功能不少支持where條件支持外鍵group等等功能,這些功能大致的流程都是差很少先從簡單的看起。下面先看如何使用mysql

package main

import (
    "fmt"
    _ "github.com/go-sql-driver/mysql"
    "github.com/panlei/gorm"
)

var db *gorm.DB

func main() {
    InitMysql()
    var u User
    db.Where("Id = ?", 2).First(&u)

}

func InitMysql() {
    var err error
    db, err = gorm.Open("mysql", "root:***@******@tcp(**.***.***.***:****)/databasename?charset=utf8&loc=Asia%2FShanghai&parseTime=True")
    fmt.Println(err)
}

type User struct {
    Id       int    `gorm:"primary_key;column:Id" json:"id"`
    UserName string `json:"userName" gorm:"column:UserName"`
    Password string `json:"password" gorm:"column:Password"`
}

func (User) TableName() string {
    return "user"
}
  1. 首先註冊對象 添加tag標註主鍵 設置數據庫column名 數據庫名能夠和字段名不同
  2. 設置table名字,若是類名和數據庫名同樣則不須要設置
  3. 初始化數據庫鏈接 建立GormDB對象 使用Open方法返回DB
  4. 使用最簡單的where函數和First來獲取 翻譯過來的sql語句就是
    select * from user where Id = 2 limit 1

源碼分析

1. DB、search、callback對象

DB對象包含全部處理mysql的方法,主要的仍是search和callbacks
search對象存放了全部的查詢條件
Callback 對象存放了sql的調用鏈 存放了一系列的callback函數git

// Gorm中使用的DB對象
type DB struct {
    sync.RWMutex                // 鎖
    Value        interface{}
    Error        error
    RowsAffected int64

    // single db
    db                SQLCommon  // 原生db.sql對象,包含query相關的原生方法
    blockGlobalUpdate bool
    logMode           logModeValue
    logger            logger
    search            *search      // 保存搜索的條件where, limit, group,好比調用db.clone()時,會指定search
    values            sync.Map

    // global db
    parent        *DB
    callbacks     *Callback        // 當前sql綁定的函數調用鏈
    dialect       Dialect           // 不一樣數據庫適配註冊sql.db
    singularTable bool
}
// search 對象存放了全部查詢的條件 從名字就能看出來 有where or having 各類條件
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
}

// Callback記錄了調用鏈 區分了update delete query create等不一樣
// 這些callback都在callback.go中的init 方法中註冊
type Callback struct {
    logger     logger
    creates    []*func(scope *Scope)
    updates    []*func(scope *Scope)
    deletes    []*func(scope *Scope)
    queries    []*func(scope *Scope)
    rowQueries []*func(scope *Scope)
    processors []*CallbackProcessor
}
2. Scope對象 每個sql操做全部的信息
// 包含每個sql操做的相關信息
type Scope struct {
    Search          *search            // 檢索條件在1中是同一個對象
    Value           interface{}     // 保存實體類
    SQL             string            // sql語句
    SQLVars         []interface{}
    db              *DB                // DB對象
    instanceID      string
    primaryKeyField *Field
    skipLeft        bool
    fields          *[]*Field        // 字段
    selectAttrs     *[]string
}
3. Open函數 建立數據庫DB對象初始化數據庫鏈接

Open函數主要是根據輸入的數據庫信息github

  1. 建立鏈接
  2. 初始化DB對象
  3. 設置調用鏈函數
  4. 發送一個ping 測試是否能可用
func Open(dialect string, args ...interface{}) (db *DB, err error) {
    if len(args) == 0 {
        err = errors.New("invalid database source")
        return nil, err
    }
    var source string
    // 接口對應database/sql接口
    var dbSQL SQLCommon
    var ownDbSQL bool

    switch value := args[0].(type) {
    // 若是第一個參數是string 則使用sql.open 建立鏈接 返回sql.Db 對象
    case string:
        var driver = dialect
        if len(args) == 1 {
            source = value
        } else if len(args) >= 2 {
            driver = value
            source = args[1].(string)
        }
        dbSQL, err = sql.Open(driver, source)
        ownDbSQL = true
        // 若是是SQLCommon 直接賦值
    case SQLCommon:
        dbSQL = value
        ownDbSQL = false
    default:
        return nil, fmt.Errorf("invalid database source: %v is not a valid type", value)
    }
    // 初始化DB對象
    db = &DB{
        db:        dbSQL,
        logger:    defaultLogger,
        // 在callback_create.go
        // callback_deleta.go
        // callback_query.go
        // callback_save.go
        // callback_update.go  等等 註冊了默認的callback
        // callback文件中的init方法中註冊了默認的callback方法
        // 主要處理的邏輯幾乎都在各個不一樣的callback中
        callbacks: DefaultCallback,
        dialect:   newDialect(dialect, dbSQL),
    }
    db.parent = db
    if err != nil {
        return
    }
    // 發送一個ping 確認這個鏈接是可用的
    if d, ok := dbSQL.(*sql.DB); ok {
        if err = d.Ping(); err != nil && ownDbSQL {
            d.Close()
        }
    }
    return
}
4. where 函數 建立數據庫DB對象初始化數據庫鏈接

其實跟where相同的還有不少好比having、group、limit、select、or、not等等其實操做都是相似的
調用DB對象函數where 在調用search對象的where
具體就是把where條件放到search對象中的whereConditions中等最後拼接sqlsql

func (s *DB) Where(query interface{}, args ...interface{}) *DB {
    return s.clone().search.Where(query, args...).db
}

func (s *search) Where(query interface{}, values ...interface{}) *search {
    s.whereConditions = append(s.whereConditions, map[string]interface{}{"query": query, "args": values})
    return s
}
4. First 函數
  1. First 建立一個Scpoe
  2. NewScope 中調用DB.clone 函數 克隆DB對象 底層指針屬性不變
  3. inlineCondition 初始化查詢條件
  4. callCallbacks函數,傳入調用鏈 for循環調用傳入的函數
func (s *DB) First(out interface{}, where ...interface{}) *DB {
    newScope := s.NewScope(out)
    newScope.Search.Limit(1)

    // callCallbacks調用query callback方法
    return newScope.Set("gorm:order_by_primary_key", "ASC").
        inlineCondition(where...).callCallbacks(s.parent.callbacks.queries).db
}
// 新建Scope
func (s *DB) NewScope(value interface{}) *Scope {
    // 克隆DB 對象
    dbClone := s.clone()
    dbClone.Value = value
    scope := &Scope{db: dbClone, Value: value}
    if s.search != nil {
        scope.Search = s.search.clone()
    } else {
        scope.Search = &search{}
    }
    return scope
}

func (scope *Scope) inlineCondition(values ...interface{}) *Scope {
    if len(values) > 0 {
        scope.Search.Where(values[0], values[1:]...)
    }
    return scope
}

// 循環調用傳入的functions
func (scope *Scope) callCallbacks(funcs []*func(s *Scope)) *Scope {
    defer func() {
        if err := recover(); err != nil {
            if db, ok := scope.db.db.(sqlTx); ok {
                db.Rollback()
            }
            panic(err)
        }
    }()
    // 使用for循環 調用回調函數
    for _, f := range funcs {
        (*f)(scope)
        if scope.skipLeft {
            break
        }
    }
    return scope
}
5. 真正查詢方法queryCallback
  1. queryCallback 方法組成sql語句 調用database/sql 中的query方法在上一篇分析中能夠看到 循環rows結果獲取數據
  2. prepareQuerySQL方法主要是組成sql語句的方法 經過反射獲取字段名代表等屬性
func queryCallback(scope *Scope) {
    if _, skip := scope.InstanceGet("gorm:skip_query_callback"); skip {
        return
    }

    //we are only preloading relations, dont touch base model
    if _, skip := scope.InstanceGet("gorm:only_preload"); skip {
        return
    }

    defer scope.trace(NowFunc())

    var (
        isSlice, isPtr bool
        resultType     reflect.Type
        results        = scope.IndirectValue()
    )
    // 找到排序字段
    if orderBy, ok := scope.Get("gorm:order_by_primary_key"); ok {
        if primaryField := scope.PrimaryField(); primaryField != nil {
            scope.Search.Order(fmt.Sprintf("%v.%v %v", scope.QuotedTableName(), scope.Quote(primaryField.DBName), orderBy))
        }
    }

    if value, ok := scope.Get("gorm:query_destination"); ok {
        results = indirect(reflect.ValueOf(value))
    }

    if kind := results.Kind(); kind == reflect.Slice {
        isSlice = true
        resultType = results.Type().Elem()
        results.Set(reflect.MakeSlice(results.Type(), 0, 0))

        if resultType.Kind() == reflect.Ptr {
            isPtr = true
            resultType = resultType.Elem()
        }
    } else if kind != reflect.Struct {
        scope.Err(errors.New("unsupported destination, should be slice or struct"))
        return
    }
    // 準備查詢語句
    scope.prepareQuerySQL()

    if !scope.HasError() {
        scope.db.RowsAffected = 0
        if str, ok := scope.Get("gorm:query_option"); ok {
            scope.SQL += addExtraSpaceIfExist(fmt.Sprint(str))
        }
        // 調用database/sql 包中的query來查詢
        if rows, err := scope.SQLDB().Query(scope.SQL, scope.SQLVars...); scope.Err(err) == nil {
            defer rows.Close()

            columns, _ := rows.Columns()
            // 循環rows 組成對象
            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)
            }
        }
    }
}
func (scope *Scope) prepareQuerySQL() {
    // 若是是rwa 則組織sql語句
    if scope.Search.raw {
        scope.Raw(scope.CombinedConditionSql())
    } else {
        // 組織select 語句
        // scope.selectSQL() 組織select 須要查詢的字段
        // scope.QuotedTableName() 獲取表名
        // scope.CombinedConditionSql()組織條件語句
        scope.Raw(fmt.Sprintf("SELECT %v FROM %v %v", scope.selectSQL(), scope.QuotedTableName(), scope.CombinedConditionSql()))
    }
    return
}

總結

這篇文章從一個最簡單的where條件和first函數入手瞭解Gorm主體的流程和主要的對象。其實能夠看出Gorm的本質:數據庫

  1. 建立DB對象,註冊mysql鏈接
  2. 建立對象 經過tag設置一些主鍵,外鍵等
  3. 經過where或者其餘好比group having 等設置查詢的條件
  4. 經過first函數最終生成sql語句
  5. 調用database/sql 中的方法經過mysql驅動真正的查詢數據
  6. 經過反射來組成對象或者是數組對象提供使用

以後咱們能夠看一些複雜的操做,好比外鍵 預加載 多表查詢等操做。json

相關文章
相關標籤/搜索