筆者在使用gorm時通常都習慣用Find,不過最近因為在查找gorm的官網時意外看到Scan的用法,就好奇查了一下他們有什麼不同,最後就整理出了這篇文章
可以先看一下Find在查詢資料時做了什麼事情(下面的code可以不用看的太認真,看下面的結論比較重要,除非你真的很想知道非常底層的部分)
func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB) {
tx = db.getInstance()
if len(conds) > 0 {
if exprs := tx.Statement.BuildCondition(conds[0], conds[1:]...); len(exprs) > 0 {
tx.Statement.AddClause(clause.Where{Exprs: exprs})
}
}
tx.Statement.Dest = dest
return tx.callbacks.Query().Execute(tx)
}
我們特別看下面這段就可以了解到,Find是在db進行Query執行前就進行結構的轉換
tx.Statement.Dest = dest
tx.callbacks.Query().Execute(tx)
再來看看Scan在查詢資料時做了什麼事情(下面的code可以不用看的太認真,看下面的結論比較重要,除非你真的很想知道非常底層的部分)
func (db *DB) Scan(dest interface{}) (tx *DB) {
config := *db.Config
currentLogger, newLogger := config.Logger, logger.Recorder.New()
config.Logger = newLogger
tx = db.getInstance()
tx.Config = &config
// 請看這邊~~
if rows, err := tx.Rows(); err == nil {
if rows.Next() {
tx.ScanRows(rows, dest)
} else {
tx.RowsAffected = 0
}
tx.AddError(rows.Close())
}
currentLogger.Trace(tx.Statement.Context, newLogger.BeginAt, func() (string, int64) {
return newLogger.SQL, tx.RowsAffected
}, tx.Error)
tx.Logger = currentLogger
return
}
// 上面的Scan裡面的tx.Rows() 就會呼叫這邊
func (db *DB) Rows() (*sql.Rows, error) {
tx := db.getInstance().Set("rows", true)
tx = tx.callbacks.Row().Execute(tx)
rows, ok := tx.Statement.Dest.(*sql.Rows)
if !ok && tx.DryRun && tx.Error == nil {
tx.Error = ErrDryRunModeUnsupported
}
return rows, tx.Error
}
// 上面的Scan裡面的tx.ScanRows(rows, dest) 就會呼叫這邊
func (db *DB) ScanRows(rows *sql.Rows, dest interface{}) error {
tx := db.getInstance()
if err := tx.Statement.Parse(dest); !errors.Is(err, schema.ErrUnsupportedDataType) {
tx.AddError(err)
}
tx.Statement.Dest = dest
tx.Statement.ReflectValue = reflect.ValueOf(dest)
for tx.Statement.ReflectValue.Kind() == reflect.Ptr {
elem := tx.Statement.ReflectValue.Elem()
if !elem.IsValid() {
elem = reflect.New(tx.Statement.ReflectValue.Type().Elem())
tx.Statement.ReflectValue.Set(elem)
}
tx.Statement.ReflectValue = elem
}
Scan(rows, tx, ScanInitialized)
return tx.Error
}
我們特別看下面這兩段就可以了解到,Scan是在db進行Query執行後才進行結構的轉換
func (db *DB) Scan(dest interface{}) (tx *DB) {
...
if rows, err := tx.Rows(); err == nil {
if rows.Next() {
tx.ScanRows(rows, dest)
} else {
tx.RowsAffected = 0
}
tx.AddError(rows.Close())
}
...
}
func (db *DB) Rows() (*sql.Rows, error) {
tx := db.getInstance().InstanceSet("rows", true)
tx.callbacks.Row().Execute(tx)
rows, ok := tx.Statement.Dest.(*sql.Rows)
...
return rows, tx.Error
}
func (db *DB) ScanRows(rows *sql.Rows, dest interface{}) error {
...
tx.Statement.Dest = dest
tx.Statement.ReflectValue = reflect.ValueOf(dest)
...
}
造成的差異為下
1.是Scan會需要透過Row()指定資料表的名稱(是真的在db的資料表名稱,users這種,不能用User)
2.Query可用的方法有差異可以在這個檔案位置看到(gorm/callbacks/callbacks.go)
Find的Query:queryCallback := db.Callback().Query()
queryCallback.Register("gorm:query", Query)
queryCallback.Register("gorm:preload", Preload)
queryCallback.Register("gorm:after_query", AfterQuery)
Scan的Query:db.Callback().Row().Register("gorm:row", RowQuery)
db.Callback().Raw().Register("gorm:raw", RawExec)
3.雖然說scan在實行時要寫sql語法比較麻煩,但相對的它指定搜尋特定欄位,也是有它的好處(避免select * 的狀況)
4.Raw只能用在Scan不能用在Finddb.Raw("SELECT name, age FROM users WHERE name = ?", "Antonio").Scan(&result) V
db.Raw("SELECT name, age FROM users WHERE name = ?", "Antonio").Find(&result) X
雖然Find可以讓我們在寫code的時候透過Preload或是Distinct之類的方法可以很輕鬆的幫我們做關聯查詢,但其實背後也犧牲了資料庫查詢的效能,如果遇到效能瓶頸時,或許把Find改成Row是一個不錯的做法。
以上提供的解法為筆者的淺見。若以上內容有誤,煩請各位讀者用力指正,謝謝。