GORM之ErrRecordNotFound採坑記錄

tyloafer發表於2019-12-22

在我印象中有個錯誤的認知:如果GORM沒有找到record,則會返回ErrRecordNotFound 的錯誤,知道上次業務中出現了bug,我才發現這個印象中的認知是錯誤的,且沒有官方文件的支援。那麼,ErrRecordNotFound 到底在什麼時候返回呢,這篇文章將會根據原始碼來進行分析一下

demo

首先我們先來看一個示例,然後,猜測一下列印的結果

package main

import (
  "fmt"

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

type User struct {
  ID    int64  `gorm:"column:id;primary_key" json:"id"`
  Name  string `gorm:"column:name" json:"name"`
  Email string `gorm:"column:email" json:"email"`
}

func (user *User) TableName() string {
  return "ranger_user"
}

func main() {
  db := open()
  user := &User{}
  users := make([]*User, 0, 0)
  err := db.Model(user).Where("id = ?", 1).First(user).Error
  fmt.Println(err, user)

  err = db.Model(user).Where("id = ?", 1).Find(&users).Error
  fmt.Println(err, user)
}

func open() *gorm.DB {
  db, err := gorm.Open("mysql",
     "user:pass@(ip:port)/db?charset=utf8&parseTime=True&loc=Local")
  if err != nil {
     panic(err)
  }
  return db
}
複製程式碼

結果:

record not found &{0  }
<nil> &{0  }
複製程式碼

綜上,可以發現,First() 函式找不到record的時候,會返回ErrRecordNotFound , 而Find() 則是返回nil,好了,這篇文章就到此結束了

當然,上面一句是開玩笑的,我可不是標題黨,沒點乾貨,怎好意思在這扯淡,下面我們開始追進原始碼

結構

這裡是後面可能會用到的一些資料結構,放在前面有個印象,便於理解

DB

// DB contains information for current db connection
type DB struct {
	sync.RWMutex
	Value        interface{}  // 這裡存放的model結構體,可通過結構體的方法,找到需要查詢的表
	Error        error        // 出錯資訊
	RowsAffected int64        // sql返回的 rows affected

	// single db
	db                SQLCommon // db資訊,這裡是最小資料庫連線所需用到的函式的interface
	blockGlobalUpdate bool
	logMode           logModeValue
	logger            logger
	search            *search   // where order group 等條件存放的地方
	values            sync.Map  // 資料結果集

	// global db
	parent        *DB    // parent db, gorm的大部分函式,都會clone一個自身,然後把被clone的物件放在這裡
	callbacks     *Callback // 回撥函式
	dialect       Dialect 
	singularTable bool

	// function to be used to override the creating of a new timestamp
	nowFuncOverride func() time.Time
}
複製程式碼

Scope

scope結構體記錄了當前對資料庫的所有操作

type Scope struct {
	Search          *search
	Value           interface{}  // 使用者通過First,Find 傳入的結果容器變數
	SQL             string
	SQLVars         []interface{}
	db              *DB
	instanceID      string
	primaryKeyField *Field
	skipLeft        bool
	fields          *[]*Field
	selectAttrs     *[]string
}
複製程式碼

SQLCommon

// SQLCommon is the minimal database connection functionality gorm requires.  Implemented by *sql.DB.
type SQLCommon interface {
	Exec(query string, args ...interface{}) (sql.Result, error)
	Prepare(query string) (*sql.Stmt, error)
	Query(query string, args ...interface{}) (*sql.Rows, error)
	QueryRow(query string, args ...interface{}) *sql.Row
}
複製程式碼

search

sql條件的搜尋語句都記錄在這裡了,最後拼接出來sql

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

記錄了各個查詢的回撥函式,相應查詢完成後,會呼叫對應的回撥函式

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
}
複製程式碼

CallbackProcessor

callback的詳情資訊,可根據這些資訊,對callback進行排序,然後再放入到 Callback 結構體 creates 等屬性中

// CallbackProcessor contains callback informations
type CallbackProcessor struct {
	logger    logger
	name      string              // current callback's name
	before    string              // register current callback before a callback
	after     string              // register current callback after a callback
	replace   bool                // replace callbacks with same name
	remove    bool                // delete callbacks with same name
	kind      string              // callback type: create, update, delete, query, row_query
	processor *func(scope *Scope) // callback handler
	parent    *Callback
}
複製程式碼

新建連線

查詢第一步,先建立好連線

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
	var dbSQL SQLCommon
	var ownDbSQL bool

	switch value := args[0].(type) {
	case string:
		// 根據 dialect 判斷資料庫驅動型別
		var driver = dialect
		if len(args) == 1 {
			source = value
		} else if len(args) >= 2 {
			driver = value
			source = args[1].(string)
		}
		// 呼叫底層的database.sql 來建立一個sql.DB
		dbSQL, err = sql.Open(driver, source)
		ownDbSQL = true
	case SQLCommon:
		// 如果原先就是 SQLCommon interface,直接拿過來使用即可
		dbSQL = value
		ownDbSQL = false
	default:
		return nil, fmt.Errorf("invalid database source: %v is not a valid type", value)
	}

	db = &DB{
		db:        dbSQL,
		logger:    defaultLogger,
		callbacks: DefaultCallback,
		dialect:   newDialect(dialect, dbSQL),
	}
	db.parent = db
	if err != nil {
		return
	}
	// Send a ping to make sure the database connection is alive.
	// 發個ping,確保連線有效
	if d, ok := dbSQL.(*sql.DB); ok {
		if err = d.Ping(); err != nil && ownDbSQL {
			d.Close()
		}
	}
	return
}
複製程式碼

可以看出,gorm.Open 也是呼叫了go提供的 sql.Open 來建立一個連結,最後ping一下,確保這個連結有效

查詢

查詢函式

連結建立完了,後面就開始進行查詢了,逐個分析查詢中的各個函式

func (s *DB) Model(value interface{}) *DB {
  // 首先clone自身,確保使用了函式後,不會影響原先的變數
	c := s.clone()
  // 把傳過來的結構體,設給Value
	c.Value = value
	return c
}
複製程式碼
// Where return a new relation, filter records with given conditions, accepts `map`, `struct` or `string` as conditions, refer http://jinzhu.github.io/gorm/crud.html#query
func (s *DB) Where(query interface{}, args ...interface{}) *DB {
	return s.clone().search.Where(query, args...).db
}

// search.Where
func (s *search) Where(query interface{}, values ...interface{}) *search {
  // 把搜尋條件追加到search的 whereConditions 屬性裡面
	s.whereConditions = append(s.whereConditions, map[string]interface{}{"query": query, "args": values})
	return s
}
複製程式碼

First

// First find first record that match given conditions, order by primary key
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
}

// NewScope create a scope for current operation
// 建立一個新的scope結構體,並記錄當前的所有操作
func (s *DB) NewScope(value interface{}) *Scope {
	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
}
複製程式碼

Find

Find方法與First的邏輯很像,First增加了一個Limit(1), 而Find沒有

// Find find records that match given conditions
func (s *DB) Find(out interface{}, where ...interface{}) *DB {
	return s.NewScope(out).inlineCondition(where...).callCallbacks(s.parent.callbacks.queries).db
}
複製程式碼

執行到這裡,我們發現,我們設定了sql的查詢條件,但是貌似這個sql還沒有執行呢,就剩下一個 callCallbacks 沒有執行了,難道在這個函式裡面,拼接sql並執行嗎,後面就繼續探索一下

callCallbacks

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 _, f := range funcs {
		(*f)(scope)
		if scope.skipLeft {
			break
		}
	}
	return scope
}
複製程式碼

就這樣,那麼 callCallbacks 到底執行了啥?

想要知道callCallbacks 到底執行了什麼,我們就要看看,呼叫callCallbacks 時,傳了什麼過來

s.NewScope(out).inlineCondition(where...).callCallbacks(s.parent.callbacks.queries).db
複製程式碼

s.parent.callbacks.queries 這個好像一直沒有看到有賦值的地方,那就只有 init() 來解釋了,那麼就看一下 gorm初始化都幹了什麼

gorm 初始化

callback_query.go

func init() {
	DefaultCallback.Query().Register("gorm:query", queryCallback)
	DefaultCallback.Query().Register("gorm:preload", preloadCallback)
	DefaultCallback.Query().Register("gorm:after_query", afterQueryCallback)
}
複製程式碼

callback_delete.go

func init() {
	DefaultCallback.Delete().Register("gorm:begin_transaction", beginTransactionCallback)
	DefaultCallback.Delete().Register("gorm:before_delete", beforeDeleteCallback)
	DefaultCallback.Delete().Register("gorm:delete", deleteCallback)
	DefaultCallback.Delete().Register("gorm:after_delete", afterDeleteCallback)
	DefaultCallback.Delete().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback)
}
複製程式碼

等等,create、update均有對應的init函式,這裡就不繼續擴充套件了

註冊回撥

// Query could be used to register callbacks for querying objects with query methods like `Find`, `First`, `Related`, `Association`...
// Refer `Create` for usage
// 建立一個CallbackProcessor 的結構體,在這個結構體上註冊資訊
func (c *Callback) Query() *CallbackProcessor {
	return &CallbackProcessor{logger: c.logger, kind: "query", parent: c}
}
複製程式碼
// Register a new callback, refer `Callbacks.Create`
func (cp *CallbackProcessor) Register(callbackName string, callback func(scope *Scope)) {
	if cp.kind == "row_query" {
		if cp.before == "" && cp.after == "" && callbackName != "gorm:row_query" {
			cp.logger.Print(fmt.Sprintf("Registering RowQuery callback %v without specify order with Before(), After(), applying Before('gorm:row_query') by default for compatibility...\n", callbackName))
			cp.before = "gorm:row_query"
		}
	}
	//繼續賦值CallbackProcessor的屬性,並把CallbackProcessor結構體追加到processors slice中
	cp.name = callbackName
	cp.processor = &callback
	cp.parent.processors = append(cp.parent.processors, cp)
  // 通過record,區分是 create update,並追加到不同的屬性上
	cp.parent.reorder()
}
複製程式碼
// reorder all registered processors, and reset CRUD callbacks
func (c *Callback) reorder() {
	var creates, updates, deletes, queries, rowQueries []*CallbackProcessor

	for _, processor := range c.processors {
		if processor.name != "" {
			switch processor.kind {
			case "create":
				creates = append(creates, processor)
			case "update":
				updates = append(updates, processor)
			case "delete":
				deletes = append(deletes, processor)
			case "query":
				queries = append(queries, processor)
			case "row_query":
				rowQueries = append(rowQueries, processor)
			}
		}
	}
	// 根據CallbackProcessor 的before after 等資訊,最這個slice進行排序,以便順序呼叫
	c.creates = sortProcessors(creates)
	c.updates = sortProcessors(updates)
	c.deletes = sortProcessors(deletes)
	c.queries = sortProcessors(queries)
	c.rowQueries = sortProcessors(rowQueries)
}
複製程式碼

到這裡,我們就看到了這些回撥函式是怎麼註冊上來的,後面就是看一下,Query對應的回撥函式,到底幹了什麼,才會導致 First 返回ErrRecordNotFound , 而Find 返回nil的區別

回撥

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(scope.db.nowFunc())

	var (
		isSlice, isPtr bool
		resultType     reflect.Type
		results        = scope.IndirectValue()
	)
	// 判斷是否有根據primary key 排序的設定,有的話,追加order排序條件
	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
	}
	// 拼接sql
	scope.prepareQuerySQL()

	if !scope.HasError() {
		scope.db.RowsAffected = 0
		if str, ok := scope.Get("gorm:query_option"); ok {
			scope.SQL += addExtraSpaceIfExist(fmt.Sprint(str))
		}
		// 執行拼接好的sql
		if rows, err := scope.SQLDB().Query(scope.SQL, scope.SQLVars...); scope.Err(err) == nil {
			defer rows.Close()

			columns, _ := rows.Columns()
			for rows.Next() {
        // 記錄RowsAffected
				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 {
        // 如果RowsAffected == 0,也就是沒有資料,且接收的結果集不是 slice,就報 ErrRecordNotFound 的錯誤
				scope.Err(ErrRecordNotFound)
			}
		}
	}
}
複製程式碼

至此,我們可以看出,跟Find 還是 First函式沒有太大的關係,而是跟傳遞的接收結果的變數有關,如果接收結果的變數時slice,那麼就不會報ErrRecordNotFound

進一步驗證

我們修改一下demo中的main函式

func main() {
	db := open()
	user := &User{}
	users := make([]*User, 0, 0)
	err := db.Model(user).Where("id = ?", 1).First(&users).Error
	db.Table(user.TableName()).Model(user)
	fmt.Println(err, user)

	err = db.Model(user).Where("id = ?", 1).Find(user).Error
	fmt.Println(err, user)
}
複製程式碼

這時候,按照我們追蹤原始碼得到的結果,應該返回

<nil> &{0  }
record not found &{0  }
複製程式碼

列印出來結果如下

<nil> &{0  }
record not found &{0  }
複製程式碼

甚是完美

總結

傳入接收結果集的變數只能為Struct型別或Slice型別,當傳入變數為Struc型別時,如果檢索出來的資料為0條,會丟擲ErrRecordNotFound錯誤,當傳入變數為Slice型別時,任何條件下均不會丟擲ErrRecordNotFound錯誤

相關文章