Gorm 中 Scan 和 Find 的区别

the difference of Gorm scan and find

foreversmart write on 2021-02-10
我们可以先看 Scan 和 Find 在 Gorm 中的接口定义
FInd
// gorm/finisher_api.go // Find find records that match given conditions 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 tx.callbacks.Query().Execute(tx) return }
Go
Execute 方法会执行 Query() callback 中的回调函数,回调函数中是它真正的执行逻辑,下面是 Query Callback 的初始注册函数,:
// gorm/callbacks/callbacks.go queryCallback := db.Callback().Query() queryCallback.Register("gorm:query", Query) queryCallback.Register("gorm:preload", Preload) queryCallback.Register("gorm:after_query", AfterQuery)
Go
Query 函数里面是具体执行的逻辑:
// gorm/callbacks/query.go func Query(db *gorm.DB) { if db.Error == nil { BuildQuerySQL(db) if !db.DryRun && db.Error == nil { // 执行数据库语句返回对应结果的行 rows, err := db.Statement.ConnPool.QueryContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...) if err != nil { db.AddError(err) return } defer rows.Close() // 扫描所有返回的结果行,把数据反序列化到目标结构中 gorm.Scan(rows, db, false) } }
Go
Scan 方法
// gorm/finisher_api.go // Scan scan value to a struct 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 { tx.AddError(err) } else { defer rows.Close() if rows.Next() { tx.ScanRows(rows, dest) } else { tx.RowsAffected = 0 } } currentLogger.Trace(tx.Statement.Context, newLogger.BeginAt, func() (string, int64) { return newLogger.SQL, tx.RowsAffected }, tx.Error) tx.Logger = currentLogger return }
Go
这个方法里面主要有两个逻辑一个是获取对应的结果行 rows, err := tx.Rows() 另一个是对结果行做扫描反序列化 tx.ScanRows(rows, dest)
首先来看下 rows, err := tx.Rows()
// gorm/finisher_api.go 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) if !ok && tx.DryRun && tx.Error == nil { tx.Error = ErrDryRunModeUnsupported } return rows, tx.Error }
Go
它的主要执行逻辑是 tx.callbacks.Row().Execute(tx) ,Execute 方法会执行 Row callback 中的回调函数,回调函数中是它真正的执行逻辑,下面是 Row 的初始化回调函数
// gorm/callbacks/callbacks.go db.Callback().Row().Register("gorm:row", RowQuery) db.Callback().Raw().Register("gorm:raw", RawExec)
Go
主要的逻辑在 RowQuery 函数中
// gorm/callbacks/row.go func RowQuery(db *gorm.DB) { if db.Error == nil { BuildQuerySQL(db) if !db.DryRun { if isRows, ok := db.InstanceGet("rows"); ok && isRows.(bool) { db.Statement.Dest, db.Error = db.Statement.ConnPool.QueryContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...) } else { db.Statement.Dest = db.Statement.ConnPool.QueryRowContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...) } db.RowsAffected = -1 } } }
Go
这里我们可以看到这个 RowQuery 函数和 Find 回调的 Query 函数逻辑几乎差不多,唯一的差别就是将结果 rows 赋值给了 Statement 里面的 Dest
Scan 方法中的另一个操作是执行 tx.ScanRows(rows, dest)
// gorm/finisher_api.go 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 { tx.Statement.ReflectValue = tx.Statement.ReflectValue.Elem() } Scan(rows, tx, true) return tx.Error }
Go
这个方法将 Statement 里面的 Dest 值改为 Scan 方法中的 Dest 结构,然后执行 Scan 函数 这个和 Find 函数 Query() 回调函数中的逻辑是一样的。
那么我们最后来看看到底 Scan 方法和 Find 方法有什么不同:
Find 在调用 Execute() 然后执行回调函数前执行了 tx.Statement.Dest = dest 修改了语句的目标 Struct,而 Scan 没有,Scan 方法是在执行完 Execute() 里面的回调函数后,在 ScanRows 方法里面调用tx.Statement.Dest = dest 。会有一个结果就是 Execute 方法会调用Parse 方法解析 Dest 或 Model 中的 struct 来得到数据库表的 Schema,而显然 Scan 方法是解析不到的。所以使用 Scan 方法的时候需要我们显示指定数据库的表名。
回调函数注册的不一样,Find 函数支持更多的 Callback 注入
queryCallback.Register("gorm:preload", Preload) queryCallback.Register("gorm:after_query", AfterQuery)
Go

「真诚赞赏,手留余香」

Foreversmart

真诚赞赏,手留余香

使用微信扫描二维码完成支付