深入了解gorm Scan的使用
前言
在使用gorm查询数据保存时,可以通过Scan
快速方便地将数据存储到指定数据类型中,减少数据的手动转存及赋值过程。
使用示例:
type Result struct {Name stringAge 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.RWMutexValue interface{}Error errorRowsAffected int64// single dbdb SQLCommonblockGlobalUpdate boollogMode logModeValuelogger loggersearch *searchvalues sync.Map// global dbparent *DBcallbacks *Callbackdialect DialectsingularTable bool// function to be used to override the creating of a new timestampnowFuncOverride 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 boolresultType reflect.Typeresults = scope.IndirectValue())...// 取出存储的destif 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 = trueresultType = results.Type().Elem()//获取slice内子元素的类型results.Set(reflect.MakeSlice(results.Type(), 0, 0))//根据子元素类型,初始化sliceif 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() {//循环处理查询到的所有rowsscope.db.RowsAffected++elem := resultsif 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 []*FieldindirectScopeValue = scope.IndirectValue()isStruct = indirectScopeValue.Kind() == reflect.Struct)for _, structField := range scope.GetModelStruct().StructFields {if isStruct {fieldValue := indirectScopeValuefor _, 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 nilif 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 structif reflectType.Kind() != reflect.Struct {return &modelStruct}...// Even it is ignored, also possible to decode db value into the fieldif 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{}//默认valuevalues = make([]interface{}, len(columns))//存储接收数据的指针类型selectFields []*Field//存储未匹配的接收filedsselectedColumnsMap = map[string]int{}//已匹配的到的列resetFields = map[int]*Field{}//需要将数据转换为非指针类型的fields)// 根据查询数据的列名循环处理for index, column := range columns {values[index] = &ignored// rows.Scan要求所有接收数据的类型为指针类型,因此需要将selectFields转换为指针类型,再接收数据selectFields = fields//接收数据fieldsoffset := 0if idx, ok := selectedColumnsMap[column]; ok {//已完成接收的fields移除offset = idx + 1selectFields = selectFields[offset:]}for fieldIndex, field := range selectFields {//循环处理剩余的fieldsif 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开发相关内容,望大家感兴趣的支持一下,在此特别感谢。
深入了解gorm Scan的使用相关推荐
- GORM报错sql: Scan called without calling Next
@GORM报错sql: Scan called without calling NextTOC GORM报错sql: Scan called without calling Next 使用gorm操作 ...
- gorm的Raw与scan
gorm的Raw与scan Raw 中文:原生的 作用:在写gorm语句时候用来写Raw sql语句(原生sql语句) gorm官方介绍Scan: https://gorm.io/zh_CN/docs ...
- Go 语言编程 — gorm ORM 框架
目录 文章目录 目录 实现一个关系型数据库应用程序需要做什么? GORM 连接数据库 表定义 Module Struct tags 表操作 db.HasTable 表是否存在 db.CreateTab ...
- gorm增删改查总结
gorm在创建表时使用CreateTable方法进行处理,其参数可以是结构体变量的地址形式,也可以是结构体的地址形式. 例如: var t Tecent db.CreateTable(&t)或 ...
- go 自定义error怎么判断是否相等_Go Web 小技巧(二)GORM 使用自定义类型
不知道大家在使用 Gorm 的时候,是否有遇到过复杂类型 ( map, struct...) 如何映射到数据库的字段上的问题? 本文分别介绍通过实现通用接口和 Hook 的方式绑定复杂的数据类型. 一 ...
- goland gorm分组查询统计_golang gorm 计算字段和获取sum()值的实现
计算表lb_ytt_user_money_log 中,字段money的和 代码如下: var total_money []int sqlstr := `select SUM(money) as tot ...
- gorm中使用where in 条件
使用一个切片来填充 ? 的位置,不需要括号,遇到切片的变量,gorm会自动补全一对括号. var sons []string var score []DepartScore ......db.Debu ...
- golang封装mysql涉及到的包以及sqlx和gorm的区别
一.前言 本篇是搬运之前的笔记,刚用golang的时候,看到mysql的封装部分,总是很好奇为什么会用到那么多的包,例如: "database/sql" "github. ...
- gorm记一次joins查询不出数据
在使用Joins查询时,使用了Scan自定义接口获取数据 type UserRouteResult struct {Id uint `json:"id"`Cover string ...
- Go语言教程第十六集 GORM详解
GORM介绍和使用 什么是ORM Object Relational Mapping:对象关系映射 结构体 和 SQL数据库存在映射,这个时候就有了ORM语句 一句话说:就是将数据库中的表数据 和 结 ...
最新文章
- 台湾证券交易开通运营现代化数据中心
- 谈谈Activity如何启动的
- View的绘制-draw流程详解
- python数组切片效率_python – 对numpy数组切片进行采样的最快方法是什么?
- 函数和常用模块【day04】:函数参数及调用(二)
- 局部钩子能防全局钩子吗_阿特的钩子成为队友的噩梦,毫无游戏体验感,小夏:当场哭了出来...
- Mac OS X下安装nvm的方法
- oracle 存储过程打印语句,oracle学习之第一个存储过程:打印Hello World
- 10 种保护 Spring Boot 应用的绝佳方法 1
- 消费者服务消费延时分析
- mysql分组查询统计求和
- 用Python实现简单的Web Server
- vmware7序列号
- Java——自定义图片和居中
- html字体颜色渐变色,css颜色渐变实例:css3文字颜色渐变的实现方法
- Mysql——分组查询
- 4. 串的【朴素模式匹配算法】、【KPM算法:求next数组、nextval数组】
- 二、RPA机器人开发基础
- 我的成神之路!Python 兵器谱(绝世神兵!收藏必备!)
- 2020高考报计算机专业指南,2020高考志愿填报最全攻略
热门文章
- 获取网站CDN加速的真实服务器IP方法
- 黑盒测试方法(五)正交实验设计方法
- 服务器raid5数据恢复成功案例,磁盘阵列数据恢复方法
- 仓库盘点的四大方法和盘点流程
- 硬件级光线追踪:移动游戏图形的变革时刻
- Mysql实现汉字首字母大写搜索
- 富爸爸实现财务自由七步骤
- PLC网关 PLC远程控制调试
- 思维模型篇:行业商业分析案例详解
- 要求用户首先输入员工数量,然后输入相应员工信息,格式为: name,age,gender,salary,hiredate 例如: 张三,25,男,5000,2006-02-15 每一行为一个员