在我印象中有個錯誤的認知:若是GORM沒有找到record,則會返回ErrRecordNotFound
的錯誤,知道上次業務中出現了bug,我才發現這個印象中的認知是錯誤的,且沒有官方文檔的支持。那麼,ErrRecordNotFound
到底在何時返回呢,這篇文章將會根據源碼來進行分析一下html
首先咱們先來看一個示例,而後,猜想一下打印的結果mysql
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 } 複製代碼
結果:git
record not found &{0 }
<nil> &{0 }
複製代碼
綜上,能夠發現,First()
函數找不到record的時候,會返回ErrRecordNotFound
, 而Find()
則是返回nil,好了,這篇文章就到此結束了github
固然,上面一句是開玩笑的,我可不是標題黨,沒點乾貨,怎好意思在這扯淡,下面咱們開始追進源碼sql
這裏是後面可能會用到的一些數據結構,放在前面有個印象,便於理解shell
// 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結構體記錄了當前對數據庫的全部操做數據庫
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 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 } 複製代碼
sql條件的搜索語句都記錄在這裏了,最後拼接出來sqljson
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 } 複製代碼
記錄了各個查詢的回調函數,相應查詢完成後,會調用對應的回調函數bash
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 } 複製代碼
callback的詳情信息,可根據這些信息,對callback進行排序,而後再放入到 Callback
結構體 creates
等屬性中markdown
// 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 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方法與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並執行嗎,後面就繼續探索一下
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初始化都幹了什麼
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錯誤