Gorm Source Analysis Simple query Analysis

Keywords: Go SQL Database MySQL JSON

Simple use

In the previous article, we have learned how to call mysql database without using orm. In this article, we will look at Gorm's source code, starting with the simplest query statement. Of course, Gorm's functions support where conditions, foreign key group s, and so on. The general process of these functions is almost from a simple point of view. Here's how to use it

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. First, the registered object adds tag to annotate the primary key to set the database column name. The database name can be different from the field name.
  2. Set the table name. If the class name is the same as the database name, no settings are required.
  3. Initialize database connection to create GormDB object and return DB using Open method
  4. Using the simplest where function and First to get the translated sql statement is
    select * from user where Id = 2 limit 1

Source code analysis

1. DB, search, callback objects

DB objects contain all the methods for handling mysql, mainly search and callbacks
The search object stores all the query conditions
Callback objects store sql call chains and a series of callback functions

// DB objects used in Gorm
type DB struct {
    sync.RWMutex                // lock
    Value        interface{}
    Error        error
    RowsAffected int64

    // single db
    db                SQLCommon  // Native db.sql object, containing query-related native methods
    blockGlobalUpdate bool
    logMode           logModeValue
    logger            logger
    search            *search      // Save search conditions where, limit, group, such as when db.clone() is called, specify search
    values            sync.Map

    // global db
    parent        *DB
    callbacks     *Callback        // Function call chain currently bound by sql
    dialect       Dialect           // Registration of sql.db with different databases
    singularTable bool
}
// The search object stores all the conditions for the query, and you can see from the name that there are various conditions 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 records the call chain differentiating update delete query create, etc.
// These callbacks are registered in the init method in callback.go
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 objects operate on all information for each sql
// Contains information about each sql operation
type Scope struct {
    Search          *search            // Retrieval condition 1 is the same object
    Value           interface{}     // Save entity classes
    SQL             string            // sql statement
    SQLVars         []interface{}
    db              *DB                // DB object
    instanceID      string
    primaryKeyField *Field
    skipLeft        bool
    fields          *[]*Field        // field
    selectAttrs     *[]string
}
3. Open function creates database DB object to initialize database connection

Open function is mainly based on the input database information

  1. Create a connection
  2. Initialization of DB objects
  3. Setting call chain functions
  4. Send a ping test to see if it's available
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
    // Interface corresponds to database/sql interface
    var dbSQL SQLCommon
    var ownDbSQL bool

    switch value := args[0].(type) {
    // If the first parameter is string, create a connection using sql.open to return the sql.Db object
    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
        // If it is SQLCommon direct assignment
    case SQLCommon:
        dbSQL = value
        ownDbSQL = false
    default:
        return nil, fmt.Errorf("invalid database source: %v is not a valid type", value)
    }
    // Initialization of DB objects
    db = &DB{
        db:        dbSQL,
        logger:    defaultLogger,
        // In callback_create.go
        // callback_deleta.go
        // callback_query.go
        // callback_save.go
        // callback_update.go, etc. registers the default callback
        // The default callback method is registered in the init method in the callback file
        // Almost all of the main processing logic is in different callback s
        callbacks: DefaultCallback,
        dialect:   newDialect(dialect, dbSQL),
    }
    db.parent = db
    if err != nil {
        return
    }
    // Send a ping to confirm that the connection is available
    if d, ok := dbSQL.(*sql.DB); ok {
        if err = d.Ping(); err != nil && ownDbSQL {
            d.Close()
        }
    }
    return
}
4. where function creates database DB object to initialize database connection

In fact, there are many similar operations like having, group, limit, select, or, not and so on.
Calling the DB object function where is calling the search object where
Specifically, place the where condition in the search object where Conditions medium and finally stitch sql

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 function
  1. First creates a Scpoe
  2. Cloning DB object's underlying pointer property by calling DB.clone function in NewScope remains unchanged
  3. inlineCondition Initializes Query Conditions
  4. callCallbacks function, the incoming call chain for loop calls the incoming function
func (s *DB) First(out interface{}, where ...interface{}) *DB {
    newScope := s.NewScope(out)
    newScope.Search.Limit(1)

    // CallCallCallbacks call the query callback method
    return newScope.Set("gorm:order_by_primary_key", "ASC").
        inlineCondition(where...).callCallbacks(s.parent.callbacks.queries).db
}
// New Scope
func (s *DB) NewScope(value interface{}) *Scope {
    // Cloning DB objects
    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
}

// Loop calls to incoming 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)
        }
    }()
    // Callback functions are called using for loops
    for _, f := range funcs {
        (*f)(scope)
        if scope.skipLeft {
            break
        }
    }
    return scope
}
5. Real query method queryCallback
  1. The queryCallback method composes SQL statements and calls the query method in database/sql. In the previous analysis, you can see the result of cyclic rows to get data.
  2. PrepareQuery sql method is mainly composed of sql statements, which acquire field names and other attributes by reflection.
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()
    )
    // Find the sort field
    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
    }
    // Prepare query statements
    scope.prepareQuerySQL()

    if !scope.HasError() {
        scope.db.RowsAffected = 0
        if str, ok := scope.Get("gorm:query_option"); ok {
            scope.SQL += addExtraSpaceIfExist(fmt.Sprint(str))
        }
        // Call query in database/sql package to query
        if rows, err := scope.SQLDB().Query(scope.SQL, scope.SQLVars...); scope.Err(err) == nil {
            defer rows.Close()

            columns, _ := rows.Columns()
            // Loop rows constitute objects
            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() {
    // If rwa, organize sql statements
    if scope.Search.raw {
        scope.Raw(scope.CombinedConditionSql())
    } else {
        // Organize select statements
        // scope.selectSQL() Organizes the fields that select needs to query
        // scope.QuotedTableName() Gets the table name
        // scope.CombinedConditionSql() Organizes conditional statements
        scope.Raw(fmt.Sprintf("SELECT %v FROM %v %v", scope.selectSQL(), scope.QuotedTableName(), scope.CombinedConditionSql()))
    }
    return
}

summary

This article starts with the simplest where condition and first function to understand the process and main objects of Gorm body. In fact, we can see the essence of Gorm:

  1. Create DB objects and register mysql connections
  2. Create objects by tag setting some primary keys, foreign keys, etc.
  3. Conditions for queries are set by where or by other means, such as group having, etc.
  4. Finally generate sql statement through first function
  5. Call methods in database/sql to drive real query data through mysql
  6. Composition of objects or array objects by reflection for use

Then we can see some complicated operations, such as foreign key preloading multi-table query and so on.

Posted by psd99 on Sat, 15 Jun 2019 12:51:44 -0700