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" }
- 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.
- Set the table name. If the class name is the same as the database name, no settings are required.
- Initialize database connection to create GormDB object and return DB using Open method
- 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
- Create a connection
- Initialization of DB objects
- Setting call chain functions
- 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
- First creates a Scpoe
- Cloning DB object's underlying pointer property by calling DB.clone function in NewScope remains unchanged
- inlineCondition Initializes Query Conditions
- 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
- 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.
- 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:
- Create DB objects and register mysql connections
- Create objects by tag setting some primary keys, foreign keys, etc.
- Conditions for queries are set by where or by other means, such as group having, etc.
- Finally generate sql statement through first function
- Call methods in database/sql to drive real query data through mysql
- 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.