在我印象中有個錯誤的認知:如果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錯誤