前言
在使用gorm查詢數據保存時,可以通過Scan
快速方便地將數據存儲到指定數據類型中,減少數據的手動轉存及賦值過程。
使用示例:
type Result struct {
Name string
Age int
}
var result Result
db.Table("users").Select("name, age").Where("name = ?", 3).Scan(&result)
// Raw SQL
db.Raw("SELECT name, age FROM users WHERE name = ?", 3).Scan(&result)
那麼,你知道:
Scan
支持哪些數據類型嗎?Scan
如何確定接收類型的數據與查詢數據之間的匹配關係的呢?
我們帶着這兩個問題去看下相關的源碼。
Scan
Scan源碼
// Scan scan value to a struct
func (s *DB) Scan(dest interface{}) *DB {
return s.NewScope(s.Value).Set("gorm:query_destination", dest).callCallbacks(s.parent.callbacks.queries).db
}
註釋中說是將value scan到struct,實際不只是,後面源碼中會給出答案。
Set
Set
是將dest存儲在DB的values(sync.Map)中,key爲gorm:query_destination
,方便後續的取出。
// Set set value by name
func (scope *Scope) Set(name string, value interface{}) *Scope {
scope.db.InstantSet(name, value)
return scope
}
// InstantSet instant set setting, will affect current db
func (s *DB) InstantSet(name string, value interface{}) *DB {
s.values.Store(name, value)
return s
}
// DB contains information for current db connection
type DB struct {
sync.RWMutex
Value interface{}
Error error
RowsAffected int64
// single db
db SQLCommon
blockGlobalUpdate bool
logMode logModeValue
logger logger
search *search
values sync.Map
// global db
parent *DB
callbacks *Callback
dialect Dialect
singularTable bool
// function to be used to override the creating of a new timestamp
nowFuncOverride func() time.Time
}
queryCallback
查詢的具體處理是在gorm/callback_query.go
文件中的queryCallback
中處理的。
queryCallback
包含了所有查詢的處理,此處僅關注Scan
的處理,其他的處理忽略。
// queryCallback used to query data from database
func queryCallback(scope *Scope) {
...
var (
isSlice, isPtr bool
resultType reflect.Type
results = scope.IndirectValue()
)
...
// 取出存儲的dest
if value, ok := scope.Get("gorm:query_destination"); ok {
results = indirect(reflect.ValueOf(value))//如果是指針取其指向的值
}
// 判斷results的類型,如果kind不爲slice或struct,則報錯
if kind := results.Kind(); kind == reflect.Slice {//slice的處理
isSlice = true
resultType = results.Type().Elem()//獲取slice內子元素的類型
results.Set(reflect.MakeSlice(results.Type(), 0, 0))//根據子元素類型,初始化slice
if resultType.Kind() == reflect.Ptr {//slice的元素爲指針類型的處理
isPtr = true//標記指針類型
resultType = resultType.Elem()//取指針指向的具體類型
}
} else if kind != reflect.Struct {//非slice及struct的報錯處理
scope.Err(errors.New("unsupported destination, should be slice or struct"))
return
}
// 準備查詢
scope.prepareQuerySQL()
// 沒有錯誤,開始查詢
if !scope.HasError() {
scope.db.RowsAffected = 0
...
// 正式開始查詢
if rows, err := scope.SQLDB().Query(scope.SQL, scope.SQLVars...); scope.Err(err) == nil {//查詢未出錯
defer rows.Close()
columns, _ := rows.Columns()//獲取列名
for rows.Next() {//循環處理查詢到的所有rows
scope.db.RowsAffected++
elem := results
if isSlice {//slice的處理
elem = reflect.New(resultType).Elem()//根據類型構造slice的elem
}
// 具體scan的處理
scope.scan(rows, columns, scope.New(elem.Addr().Interface()).Fields())
if isSlice {//slice數據的組裝
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 {//未查詢到數據,需要注意的是:僅struct時會報錯,slice並不會報錯
scope.Err(ErrRecordNotFound)
}
}
}
}
需要注意的是: queryCallback
中只檢查類型是slice或struct及它們的指針類型,所以Scan至少要求接受數據的類型是slice或struct及它們的指針類型。
queryCallback
的關於Scan的處理過程大致如下:
- 根據key取出存儲在values中的dest,獲取其(指針的)值results
- 判斷results的類型
- slice處理,獲取slice內子元素的類型,初始化slice
- 非struct及slice報錯
- 查詢數據出錯報錯處理
- 查找數據未出錯
- 獲取列名
- 循環將數據scan到elem中
- 若是slice,將elem存入slice中
- 記錄獲取到的數據條數
- 未查找到數據,且不是slice的報未查找到錯誤
獲取接收數據的fields
// Fields get value's fields
func (scope *Scope) Fields() []*Field {
if scope.fields == nil {
var (
fields []*Field
indirectScopeValue = scope.IndirectValue()
isStruct = indirectScopeValue.Kind() == reflect.Struct
)
for _, structField := range scope.GetModelStruct().StructFields {
if isStruct {
fieldValue := indirectScopeValue
for _, name := range structField.Names {
if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() {
fieldValue.Set(reflect.New(fieldValue.Type().Elem()))
}
fieldValue = reflect.Indirect(fieldValue).FieldByName(name)
}
fields = append(fields, &Field{StructField: structField, Field: fieldValue, IsBlank: isBlank(fieldValue)})
} else {
fields = append(fields, &Field{StructField: structField, IsBlank: true})
}
}
scope.fields = &fields
}
return *scope.fields
}
GetModelStruct
是一個超長長長的func(近500行代碼),看着頭皮發麻,主要是ModelStruct(聲明數據結構的struct)的解析處理。好消息是,如果你比較數據gorm的model規則,這部分不需要具體到每一行去看,着重點關注下面幾行代碼即可。
func (scope *Scope) GetModelStruct() *ModelStruct {
var modelStruct ModelStruct
// Scope value can't be nil
if scope.Value == nil {
return &modelStruct
}
reflectType := reflect.ValueOf(scope.Value).Type()
for reflectType.Kind() == reflect.Slice || reflectType.Kind() == reflect.Ptr {
reflectType = reflectType.Elem()
}
// Scope value need to be a struct
if reflectType.Kind() != reflect.Struct {
return &modelStruct
}
...
// Even it is ignored, also possible to decode db value into the field
if value, ok := field.TagSettingsGet("COLUMN"); ok {
field.DBName = value
} else {
field.DBName = ToColumnName(fieldStruct.Name)
}
modelStruct.StructFields = append(modelStruct.StructFields, field)
}
...
return &modelStruct
}
前面是對接收數據類型的檢查,要求子元素必須是struct或其指針類型,否則返回空的ModelStruct。因此,Scan支持的數據類型僅爲struct及struct slice以及它們的指針類型。如此,回答了問題1
。
最後幾行的代碼意思是:如果指定對應column的列名,則使用指定的列名,否則使用默認規則主動將key轉換成對應的列名。
再回過頭來看Fields,主要是獲取struct(或其指針類型)的fields並完成fieldValue的封裝。
scan
scan
是具體將數據存入對應fields的過程。
func (scope *Scope) scan(rows *sql.Rows, columns []string, fields []*Field) {
var (
ignored interface{}//默認value
values = make([]interface{}, len(columns))//存儲接收數據的指針類型
selectFields []*Field//存儲未匹配的接收fileds
selectedColumnsMap = map[string]int{}//已匹配的到的列
resetFields = map[int]*Field{}//需要將數據轉換爲非指針類型的fields
)
// 根據查詢數據的列名循環處理
for index, column := range columns {
values[index] = &ignored
// rows.Scan要求所有接收數據的類型爲指針類型,因此需要將selectFields轉換爲指針類型,再接收數據
selectFields = fields//接收數據fields
offset := 0
if idx, ok := selectedColumnsMap[column]; ok {//已完成接收的fields移除
offset = idx + 1
selectFields = selectFields[offset:]
}
for fieldIndex, field := range selectFields {//循環處理剩餘的fields
if field.DBName == column {//比對查詢數據的列名與接收數據的列名,一致則處理數據
if field.Field.Kind() == reflect.Ptr {//指針類型的處理,直接取指針存入
values[index] = field.Field.Addr().Interface()
} else {// 非指針類型,需要先存指針用以接收數據,後續需要重置爲非指針類型
reflectValue := reflect.New(reflect.PtrTo(field.Struct.Type))
reflectValue.Elem().Set(field.Field.Addr())
values[index] = reflectValue.Interface()
resetFields[index] = field//需要接收數據後處理
}
selectedColumnsMap[column] = offset + fieldIndex //記錄已匹配的列
if field.IsNormal {
break
}
}
}
}
scope.Err(rows.Scan(values...))//接收數據,rows.Scan要求所有接收數據的類型爲指針類型
for index, field := range resetFields {//非指針類型需要將接收到數據類型轉換
if v := reflect.ValueOf(values[index]).Elem().Elem(); v.IsValid() {
field.Field.Set(v)
}
}
}
// Scan copies the columns in the current row into the values pointed
// at by dest. The number of values in dest must be the same as the
// number of columns in Rows.
//
// Scan converts columns read from the database into the following
// common Go types and special types provided by the sql package:
//
// *string
// *[]byte
// *int, *int8, *int16, *int32, *int64
// *uint, *uint8, *uint16, *uint32, *uint64
// *bool
// *float32, *float64
// *interface{}
// *RawBytes
// *Rows (cursor value)
// any type implementing Scanner (see Scanner docs)
//
// In the most simple case, if the type of the value from the source
// column is an integer, bool or string type T and dest is of type *T,
// Scan simply assigns the value through the pointer.
//
// Scan also converts between string and numeric types, as long as no
// information would be lost. While Scan stringifies all numbers
// scanned from numeric database columns into *string, scans into
// numeric types are checked for overflow. For example, a float64 with
// value 300 or a string with value "300" can scan into a uint16, but
// not into a uint8, though float64(255) or "255" can scan into a
// uint8. One exception is that scans of some float64 numbers to
// strings may lose information when stringifying. In general, scan
// floating point columns into *float64.
//
// If a dest argument has type *[]byte, Scan saves in that argument a
// copy of the corresponding data. The copy is owned by the caller and
// can be modified and held indefinitely. The copy can be avoided by
// using an argument of type *RawBytes instead; see the documentation
// for RawBytes for restrictions on its use.
//
// If an argument has type *interface{}, Scan copies the value
// provided by the underlying driver without conversion. When scanning
// from a source value of type []byte to *interface{}, a copy of the
// slice is made and the caller owns the result.
//
// Source values of type time.Time may be scanned into values of type
// *time.Time, *interface{}, *string, or *[]byte. When converting to
// the latter two, time.RFC3339Nano is used.
//
// Source values of type bool may be scanned into types *bool,
// *interface{}, *string, *[]byte, or *RawBytes.
//
// For scanning into *bool, the source may be true, false, 1, 0, or
// string inputs parseable by strconv.ParseBool.
//
// Scan can also convert a cursor returned from a query, such as
// "select cursor(select * from my_table) from dual", into a
// *Rows value that can itself be scanned from. The parent
// select query will close any cursor *Rows if the parent *Rows is closed.
func (rs *Rows) Scan(dest ...interface{}) error {
rs.closemu.RLock()
if rs.lasterr != nil && rs.lasterr != io.EOF {
rs.closemu.RUnlock()
return rs.lasterr
}
if rs.closed {
err := rs.lasterrOrErrLocked(errRowsClosed)
rs.closemu.RUnlock()
return err
}
rs.closemu.RUnlock()
if rs.lastcols == nil {
return errors.New("sql: Scan called without calling Next")
}
if len(dest) != len(rs.lastcols) {
return fmt.Errorf("sql: expected %d destination arguments in Scan, not %d", len(rs.lastcols), len(dest))
}
for i, sv := range rs.lastcols {
err := convertAssignRows(dest[i], sv, rs)
if err != nil {
return fmt.Errorf(`sql: Scan error on column index %d, name %q: %v`, i, rs.rowsi.Columns()[i], err)
}
}
return nil
}
scan
的大致處理過程:
- 根據查詢數據列名columns循環
- 根據接收數據的fileds循環
- 比對fields中的列名field與columns中列名column,
- 若一致,確認field的類型,如果是指針類型,則直接取指針存入values中;否則,創建指針存入values,再記錄到reset中,方便後續處理。
- 調用sql.Scan將數據賦值到對應的values中
- 對於非指針類型的values,更新其值爲指針指向的值
scan
中關於查詢與接收數據的匹配是根據列名進行匹配,而列名是根據其struct的model規則指定的,因此爲保證數據能準確的Scan到,則要求接收數據的列名必須與查詢數據結構的列名對應。此處回答了問題2
。
結合Fields中的非struct類型,values爲空,將不會接收到任何數據。
總結
gorm的Scan
支持接收的數據類型是struct、struct slice以及它們的指針類型(A、[]A、[]*A、*A、*[]A、*[]*A
),鑑於是接收數據作其他處理,實際使用的都是指針類型。
需要注意的是:使用其他類型的slice並不會報錯,但是接收不到任何數據。
gorm的Scan
是根據列名進行數據匹配的,而列名是通過struct指定或自動轉換的,這就要求接收數據的與查詢數據的最終列名必須一致才能正常匹配,尤其是需要自定義新名稱時,就需要添加gorm:"column:col_name"
的tag纔行。
公衆號
鄙人剛剛開通了公衆號,專注於分享Go開發相關內容,望大家感興趣的支持一下,在此特別感謝。