go-gorm源码分析(二)

October 28, 2024

前文分析了go-gorm的大致结构,包括初始化以、连接管理以及SQL构建等,本文分析gorm下通过ORM(对象关系映射)读写数据的基本原理及执行逻辑。

ORM简介

ORM(对象关系映射)是gorm提供的核心能力,实现了golang下数据类型和DB中数据的映射,开发人员可以通过操作对象的属性和方法,来间接操作数据库中的数据,而无需编写复杂的SQL语句。

典型用法:

Loading...

本文重点介绍创建(Create)和读取(Query)相关逻辑,以及其依赖的核心数据结构。

核心结构

Processors

Processor负责gorm核心逻辑的调度,CRUD的逻辑存在很大的差异,但是为了执行流程的统一,gorm使用 processor + callbacks 的方式来实现各类操作。优点:

  • 流程统一:各Finisher Method 的执行最终都会走到 callback.Execute 中,将各类操作的差异性体现在 callbacks 中,使得整体流程无需特殊处理。
  • callback 复用:典型的事务处理(开启、关闭)可以被创建、更新等场景复用。
  • 灵活性: callback链条可以根据dialector定制(删除、替换),可以指定依赖关系等,方便兼容各类DB。

存储结构

gorm 将DB中的各种DB相关的操作拆解到各个callback中,然后使用processor来集合CRUD各类操作所需的callback列表

在DB对象gorm.DB下会存储一个callbacks指针,DB Open时会初始化callbacks对象,并分配6个processor对象,分别用于API后续的创建、查询、更新、删除、Exec、Raw执行。各Process使用统一的存储结构,差异点主要在于参数(Clauses)以及callbacks列表存在差异。

Loading...
  • Clauses 的作用前文已经介绍过了,此处不再重复。
  • fns:处理后的callbacks列表,用于最终执行gorm主要逻辑。
  • callbacks :原始注册的callbacks。

processor下提供了 Register、Rmove、Replace 几个API,用于挂载处理函数callback列表。在 diactor 的初始化过程中,会调用 callbacks.RegisterDefaultCallbacks(),注册各个processor所需的callbacks列表。

Loading...

可见写场景下DB的核心逻辑基本都在callbacks链表中覆盖到了,包括开启事务、处理前置关系、构建SQL及执行、处理后置关系以及结束事务等。其中事务处理、关系处理相关的回调函数会被update、delete等processor复用。

后续在 Finisher Method 被调用时,会触发API对应processor的Execute,在Execute中会触发callback链条的调用。

Loading...

注册流程

分析注册callback的几个细节,Processor提供了几个API,用于操作callback列表。包括:

Loading...

RegisterDefaultCallbacks 中只有Register的操作,可见gorm为各类dialector提供了一套默认的callback列表,同时也提供了修改、替换callback的能力,以适配各dialector的差异。

在Register、Remove、Replace操作中,只是向 []*callback 中追加了一条带标识(remove、replace)的记录,具体如何生效于 fns 链呢?

Loading...

添加到callbacks列表之后,需要调用一次 compile 才会形成最终的 fns 链。其主要目的是实现链表增删改的逻辑。

最后还会调用sortCallbacks对callbacks进行排序。大致方法是通过callback中指定的 before、after 字段指定的依赖顺序,调整callback链表的顺序,并存储到 fns 链表中。不过在默认的callbacks注册中没有见到指定依赖的注册,猜测在部分dialector中有使用。

至此,callback链就构造完毕了,在 callback.Execute 中会根据API操作类型选择不同的processor进行调用,从而实现创建、读取等操作的目的。

模型Models

模型(Models)的目的是为DB和结构体(Go structs)之间建立一个映射,从而简化数据库的交互。CRUD通常也是基于Model进行,但模型不是必须的,也有使用 map[string]interface{} 作为存储对象Dest的场景,这时不需要指定Model。

一个典型的模型定义如下:

Loading...

包括以下几部分信息:

  • gorm.Model:内嵌模型,非必须。也可以自定义主键等。注意内嵌模型虽然在结构体内是作为struct的嵌套字段,但是gorm在DB操作上是作为其他字段的同级字段使用的。比如在查询的SQL语句应该是 select ID, CreateAt, Name, Age 而非 select model.ID, model.CreateAt, Name, Age
Loading...
  • 用户自定义字段和数据类型。默认情况下,每个字段对应DB表中的一个field,也可以通过tag过滤掉DB相关的映射关系。可以使用tag定义来跳过部分非db字段的映射关系。
  • Tag:非必须。用于指定DB相关的约束信息,包括数据类型、大小、允许为空、PrimaryKey等信息。参考官方文档

API提供了两种方法可以指定模型:

Loading...

方法一:通过Model函数指定。

方法二:在Finisher Method中直接指定Dest对象,如果没有指定Model,则会直接使用Dest对象作为model。二者间可以相互替换。

Loading...

在callback执行(SQL拼接以及执行)之前,会根据Model解析(parse)出所需的Schema信息,以及内部的fields列表。供后续的CURD操作使用。

如果二者都指定呢? 比如 Smart Select Field 特性。

gorm在实际执行上会灵活选择。事务本身的schema还是优先使用 stat.Model,但在实际需要时,比如存储Dest也会重新解析Dest结构。

Fields

Fields是Schema中的核心数据结构,定义了结构体的各字段(field)详细信息,包括字段名称、数据类型、大小、权限、默认值等。主要用于API执行时从中读取字段值、写入字段数据等。先看Fields的定义以及解析过程。

struct下存在几种类型的field:

  • 基础类型:比如 Name string 。这种field通常作为与DB表中的某个字段行程映射关系。
  • 结构体struct,分位以下几类
    • 匿名(Anonymous)或者嵌套(带有 embeded 标签):此类struct下的所有fields会被加入到父struct中的Fields列表中。而struct本身会被忽略。子fields的作用与基础类型基本一致。
    • Relation struct:M2O、O2M、O2O、M2M等关系的field。这种struct本身会作为一个子field存储在父struct的field列表中。struct本身不会作为一个独立字段,而是作为关系表参与。
    • 实现了 Valuer/Scanner interface 的struct:这种也对应了一个db表中的某个字段,表示需要对db的值做特殊处理。参考data-types
    • 特殊类型的struct:比如 Time、Bytes。此类虽然是struct,字段会在gorm中做特殊处理。
  • 不参与gorm映射的struct或者基础类型:使用tag过滤掉,表示不参与CRUD操作,否则解析时会报错。

field的定义如下:

Loading...

字段解析在 schema.ParseField() 中完成,解析出的字段信息大概分为以下几类:

  • 基础信息:结构体字段名(reflect.StructField.Name)、db字段(DBName)名、数据类型。
  • 字段约束类信息:是否为pk,允许为空、大小、精度。基本上都是通过tag信息指定。
  • 权限控制类字段:是否允许读、写、更新。相关API中会校验其权限。其值也是通过tag指定。
  • 反射相关信息:包括字段 reflect.Type、IndirectFieldType、reflect.StructField。
  • setter、getter等函数,用于API中读写字段值。

重点字段解析:

字段名及数据类型

  • Name:对应struct中的字段名
  • DBName:通过tag指定的db字段名。未指定默认使用字段名进行转换(naming)。
  • BindNames:字段的绑定名称,默认为结构体中的字段名称,当为嵌套字段时,也会追加嵌套字段的名称。比如 ["Model", "ID"]。用于后续的关系处理。
  • EmbeddedBindNames:与bindNames相同,不包括Anonymous struct
  • DataType:tag指定的数据类型。也可以通过interface指定。见下面设置方法。
  • GORMDataType:与DataType一致,除了tag写入的datatype除外

数据类型包括:

Loading...

也可以自定义数据类型。数据类型有几种设置方法:

  1. gorm自动映射:gorm中会根据golang struct字段数据类型进行推论。比如 reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64 这些类型会映射为 Int
  2. 字段类型实现了 GormDataTypeInterface 接口,则使用接口返回值作为gorm数据类型。
  3. 使用 tag 指定类型名。
  4. 实现了Serializer接口的字段,字段类型会被强制为String。
  5. 如果为struct字段,且没有实现 GormDataTypeInterface
  6. 空权限字段,datatype为空

 

默认值

  • HasDefaultValue:是否有指定默认值标签(Default Tag)或者 AUTOINCREMENT,则HasDefaultValue为true。
  • DefaultValueInterface:默认值interface指针,可能为bool、int等类型。可以指定 Default tag标签,但是不指定默认值。比如 gorm:"default:(-)"。如果设置 AUTOINCREMENT 也会将 HasDefaultValue 设置为true,而DefaultValueInterface值为空。
  • DefaultValue:tag中指定的默认值string。

通常情况下 HasDefaultValueDefaultValueInterface 是要么同时存在,要么同时不存在的。但是有些特殊场景在gorm中无法/无需生成默认值,可能需要依赖DB产生的场景。这种字段部分API上需要做特殊处理。比如在 create 时需要跳过此类字段的值写入,但读取不做特殊处理。

 

权限相关

  • Creatable:可写
  • Updatable:可更新
  • Readable:可读

相关接口会校验权限。比如 Query 接口会校验字段是否具有 Readable 权限。

 

匿名和EMBEDDED 结构体:

匿名结构体中的字段,会被插入到父结构体中。Embedded Struct也可以实现相同的效果,所以gorm中的处理逻辑二者基本一致。

  • EmbeddedSchema:嵌套或匿名结构体的schema。此处会递归解析。
  • Schema:当存在多层嵌套时,Schema指向最外层(根)struct的schema
  • OwnerSchema:父struct的schema
  • EmbeddedBindNames:embedded结构体中的field,会在EmbeddedBindNames的最前面插入结构体名称。
  • StructField: reflect.StructField 对象。当其为嵌套或者匿名字段时,由于嵌套或者匿名字段会被加入到父struct的fields列表中,所以其index会被修改,加入struct自身的index。
💡
结构体在golang的反射机制中,会被作为一个独立的struct field对象,如果需要获取其字段,需要递归解析struct,这样对gorm很不友好。在gorm的语义中,匿名和Embedded 结构体下的字段与其他基础类型的字段没有差别,会被加入到父struct的fields列表中。所以其解析逻辑基本也是围绕实现这一点而实现的。
Loading...
Loading...

匿名和embedded fields在被解析后会被加入到schema.fields列表,与其他非struct类型的字段同级。如果存在多级嵌套,会递归解析并加入到最外层struct的fields列表,与其他字段归属同一层级

Loading...

 

值读写相关的函数(ValuerAndSetter

  • ReflectValueOf

用来获取struct中的字段值(reflect.Value),用于通过反射写入新值到字段中 。

💡
golang中通过反射来设置字段值的方法:
  1. 获取字段reflect.Value。在struct中只能通过 reflect.Indirect(value).Field(index) 来或者 reflect.Value对象。
  2. 调用reflect.Value对象的 SetXXX 方法。比如 SetInt、SetBool 等。或者直接调用 Set(需要校验类型可转换ConvertibleTo)

由于Fields列表中存在嵌套和匿名struct字段,所以不能直接使用 refelct.Field[index] 来获取reflect.Value对象,否则获取到的可能是嵌套struct字段。所以golang也提供了相关解决方法:

Loading...

对于嵌套struct,需要调用 value.FieldByIndex 并告知嵌套字段的路由信息。在FieldByIndex中会根据路由自顶向下的解析各struct,获取到最底层的基础类型字段。

但FieldByIndex有个问题,如果嵌套结构体为空指针,则会抛出异常(因为未分配内存)。FieldByIndex中不会主动初始化指针对象。所以gorm实现了自己的FieldByIndex,即 field.ReflectValueOf 方法,内部包含了(空)指针的处理逻辑。字段路由信息保存在 field.StructField.Index 数组中。

Loading...

主要作用:ReflectValueOf的作用类型与golang中 reflect.Field.Index() ,用于赋值函数 fields.Set 中设置字段的值等场景。如:

Loading...
  • ValueOf

与上面的的ReflectValueOf基本一致。除了返回的是字段值 reflect.Value的Interface对象。另外还会返回是否为空的标识。

主要作用:用于在API中获取字段的值指针(interface)。

  • Set

字段赋值函数。主要逻辑是对 ReflectValueOf().SetXXX的封装。会根据Value类型调用不同Set方法。

  • Serializer

对应的Serializer的interface值。如果字段实现了Serializer相关的接口(Scan、Value)。会对field相关的Set、Valueof等函数进行替换。

  • NewValuePool

用于构造 field.IndirectFieldType 对象的内存池( sync.Pool )。

Schema

除field外,来看看schema中的其他信息。

Loading...

schema解析在 schema.Parse() 中完成,其内容主要包括:

  • struct及对应表信息,包括表名、struct类型(reflect.Type)。
  • 字段相关信息:包括字段列表(内含struct字段信息以及对应的表字段信息)、pk字段列表、DB字段名到字段的映射、有默认值字段。
  • Clauses:通用clause,比如软删除相关的DeleteAt。执行SQL时会被合并到statment的clause中。
  • hook标识:BeforeXXX、AfterXXX等hook,会在解析schema后记录响应的hook开启标识,在callback.Execute的回调链中被执行。
  • cacheStore:指向DB对象的cacheStore。用于缓存schema,避免重复解析。

重点字典解析:

基础字段:

  • Name: modelType.Name
  • ModelType:模型对应的reflect.Type。如果是指针、数组等,会取真实struct的反射类型。
  • tableName:模型对应的表名。可以通过多种方式设置表名。包括: Tabler() inerfaceTablerWithNamer interfaceembeddedNamer
  • cacheStore:指向DB对象的cacheStore。用于缓存schema,避免重复解析。其key值默认为 modelType 。即每个struct类型只会被解析一次(性能优化)。

 

字段(field)相关:

  • Fields:struct下所有的字段列表。

    字段可以分类几类:

    1. go基础类型字段:比如string、int等类型的field
    2. 匿名、embedded类型字段:此类字段本身不会加入字段列表,而是将struct下的所有字段加入到fields列表。
    3. 普通struct类型字段:通用用于存储关系数据。此类字段会作为一个单独字段加入字段列表。
    4. ignored field:tag中指定了ignored标识的字段,此类字段不参与grom的处理逻辑。
  • FieldsWithDefaultDBValue:有指定默认标识,但是没有默认值的fields列表。即依赖DB默认值,like gorm: "Default:(-)" 。参考 Default Value

 

用于查找field的几个map。

  • FieldsByDBName:field.DBName -> Field 的映射。即db字段名对应的field对象映射。注意:没有db字段映射的field不会加入此集合。比如关系类struct。
    • 注意:多个字段使用同一个DBName时的优先级:首次出现、有权限且最短路径优先级(即当前ID 优先级 > embededModule.ID)。
  • FieldsByName:field.Name → Field 的映射,即结构体字段名(reflect.StructField.Name)到field对象的映射。
  • FieldsByBindName:field.BindNames → Field的映射,即绑定名称(含嵌套结构体名称前缀的reflect.StructField.Name)到field对象的映射。

 

主键(pk)相关:

  • PrimaryFields:设置有 PrimaryKey 标识的field列表。
  • PrioritizedPrimaryField:优先主键。 id/ID > PrimaryFields[0]。使用在 FindInBatches 之类的场景。
  • PrimaryFieldDBNames:主键fields列表对应的db字段名称(DBName)列表。

 

hook标识:用于在CRUD中判断是否需要执行相关hook

Loading...

 

Clause

可以给struct中的各字段自定义各种clause,这些clause会被作为公共clause追加到该model所有的CRUD的sql中。比如软删除的clause就是通过在 gorm.Model.DeleteAt字段中添加了各种cluase实现的。

Loading...

 

关系:

RelationShips存储所有关系

Loading...

关系部分后面再详细分析。

 

其他

注意在解析schema时有对cacheStore多次Load、LoadOrStore,是为了考虑多goroutine并行的情况。

Create流程

解析关系分析完毕后,再分析Create执行的流程。先看API做了什么:

Loading...

仅设置了目标对象值 Statement.Dest,用来传入会话要插入到DB的记录,同时也用来存储返回值。比如DB返回值autoincrement id、tag定义默认值等。

Statement.Dest支持以下结构:

  • struct 或者 struct指针
  • struct slice
  • map[string]interface{}
  • []map[string]interface{}

 

callbacks.Create()返回的是Create场景对应的processor,在其Execute中主要就是调用Create场景的回调callback。包括:

  • BeginTransaction:开启事务,参见 DB.Begin() 中的实现。
  • BeforeCreate:调用BeforeSave、BeforeCreate hooks(如果有)。为什么会连续调用两个hook?
  • SaveBeforeAssociations:create前的关系处理。对于BelongsTo关系,其结果影响到当前表,所以需要先处理关系数据。关系部分再详细分析。
  • Create:create主要逻辑
  • SaveAfterAssociations:create后关系处理,主要是对于HasOne、HasMany、Many2Many关系的处理。关系部分再详细分析。
  • AfterCreate:调用AfterCreate、AfterSave hooks(如果有)
  • CommitOrRollbackTransaction:根据db.Error 对象判断是需要 commit 还是 rollback。

事务之类的callback比较简单,主要关注Create callback的实现。

Create主逻辑

Create() callback负责了create接口的主要逻辑,其主要内容如下:


  1. DB异常检测

    如果 db.Error 不为 nil,则直接返回,不执行任何操作。

    因为processor的callback链条中间不会退出,所以需要提前对error进行检查。如果db有异常,不再进行插入处理。执行下一个callback即可。

  2. schema公共clause处理

    schema公共clause中的CreateClauses,会被添加到本次create操作中(如果有)。

    ps:软删除字段DeleteTime没有对create场景进行特殊处理,因为默认为空。而CreatedAt、UpdatedAt会在ParseField中打上AutoUpdateTime、AutoCreateTime的标识。

  3. Returning子句的处理

    如果DB支持RETURNING(参考 go-gorm/mysql 中的CreateClauses )即表明需要在 insert 语句之后插入 RETURNING子句,以获取DB写入的字段内容。

    注意RETURNING字段的选择,仅选择 FieldsWithDefaultDBValue 字段,即依赖DB默认值的字段。其他类型的字段gorm自己可以生成(所以结构体默认值的字段定义需要与DB中的保持一致)。

  4. ConvertToCreateValues:将dest转换为clause条件

    构建插入SQL之前还需要构建 clause.Values ,此clause的主要目的是生成SQL中的 (field1, field2) valuels (?, ?) 部分,ConvertToCreateValues的主要目的就是根据传入的Dest结构生成对应的clause。

    Loading...

    看value clause,其核心结构由两部分组成:字段名列表以及对应的插入值列表(二维数组以支持批量插入)。所以ConvertToCreateValues的逻辑的重点也是在于生成此两个字段列表。后面详细展开。

  5. Build SQL

    Statement.Build 构建插入SQL。参考之前的SQL构建流程。构建完毕的sql存储在 statement.SQL 中。

  6. 执行SQL

    如果配置了 RETURNING 子句并且支持(supportReturning 为 true),则使用 db.Statement.ConnPool.QueryContext 执行查询并处理返回的行。

    否则,使用 db.Statement.ConnPool.ExecContext 执行 SQL 语句

  7. 处理插入结果

    如果支持 RETURNING 子句,则可能直接通过查询结果来填充目标对象。

    如果不支持 RETURNING 子句,则尝试从数据库获取最后插入的 ID(LastInsertId),并根据配置和 Schema 信息将其赋值给目标对象或结构体中的自增主键字段。

    根据Dest类型,赋值方式也会有差异:

    • map[string]interface{}*map[string]interface{} :直接使用 PrioritizedPrimaryField.DBName 作为key,写入map即可。
    • []map[string]interface{}, *[]map[string]interface{}: 对每项按照上面的方式赋值。insertID按照 schema.DefaultAutoIncrementIncrement 递增。
    • Struct、*Struct :根据 field.ValueOf() 返回的是否为空来判断是否需要覆盖struct中的原值。如果为空,则使用 field.Set() 将field对应的值设置为insertID。
    • Slice/Array of truct、*Struct :按照上面struct的方式对每项赋值。insertID按照field指定的步长 field.AutoIncrementIncrement 递增

    此外,对数组类的赋值时,如果 dialector 的配置指定了 LastInsertIDReversed 参数,则表示array、slice中的对象需要反向遍历。即lastID返回的是最后一条数据的id。

 

ConvertToCreateValues怎么做?


ConvertToCreateValues的功能是根据传入的Dest对象,生成对应的Clause条件,所以其核心逻辑就两件事:选择插入db的字段 和 提供对应的值。包括:

  • 字段:用户使用Select选择的字段 或者 从struct中解析出的field、map中解析出的key列表。
  • 值:用户提供的值以及gorm提供的默认值。

由于Dest支持多种类型,所以其处理方式也各异,看两种典型的结构,map和struct分别如何处理:

mapValue类型的处理:

  1. SelectAndOmitColumns:获取当前查询(或插入)操作中要包含的列(selectColumns)和不允许的列(restricted)。
  2. 排序key:取出mapValue中所有的keys,并排序。排序是为了确保多次生成的SQL语句中的列顺序是一致的,之前也介绍过连接池会根据sql缓存prepare结果。
  3. 遍历键并构建clause.Values插入到clause.Values
    1. Column:对应字段对象 clause.Column。默认为key名称,如果有 schema,则会根据key名称查找field,并使用 field.DBName 。
    2. Value:map数据的value值,不做任何处理。

对于map的扩展类型( *map[string]interface{} []map[string]interface{} *[]map[string]interface{} )都是类似的。

 

struct类型的处理

  1. SelectAndOmitColumns:获取当前查询(或插入)操作中要包含的列(selectColumns)和不允许的列(restricted)。
  2. 设置字段对象 values.Columns 和值对象 values.Values ,分两种情况:
    1. 需要gorm提供默认值的字段:这里包括了使用Tag定义了明确DefaultValue的字段 和 没有指定默认Tag(HasDefaultValue为false)的字段。这类字段在写入db的value选择时优先使用struct field中的原值(通过 field.ValueOf 获取字段值)。单如果用户为设置其值(isZero为空),则使用 DefaultValueInterface 生成默认值 或者 当前时间填充带有 AutoCreateTime、AutoUpdateTime标识的字段。
    2. 字段在FieldsWithDefaultDBValue 中,此类型的字段表示DB会提供默认值,不需要gorm提供。在插入时需要跳过此字段,除非struct中的字段值非空,表示需要插入用户指定的明确值。

最后,还需要对 ON CONFLICT 进行处理,如果有 clause.OnConflict 条件,且开启了 UpdateAll,需要对 onConflict 进行处理,即插入的字段也需要追加对应的 DO UPDATE SET 子句。

 


注意点

  • 非RETURNING的场景(比如mysql)auto_increment的处理,批量插入时,gorm中是使用lastID+自增步长的方式本地计算的。如果DB有修改自增步长,而tag没有指定准确的值,就会导致批量插入时id不准确的情况出现。
    Loading...

Query流程

Find API为例,来看看query的流程。

Loading...

API跟上面的逻辑基本一致,仅设置 Dest 对象,用来存放查询结果。与Create一样,Dest也支持map和struct以及对应指针、数组。

Query的processor有个三个callbak:

  • Query:主逻辑,执行查询
  • Preload:执行Preload逻辑,为何预加载在Query之后?
  • AfterQuery:执行 AfterFind hook
Loading...

 

Query callback中分位两步:构建SQL(BuildQuerySQL) 以及 执行(gorm.Scan)。

BuildQuerySQL

  1. Schema.QueryClauses 追加到本次事务的clause中。当前主要是加入软删除相关的字段。然后开始SQL的构建。
  2. PrimaryFields 字段处理:根据schema中的Schema.PrimaryFields列表,检查struct中对应的字段是否为zero。如果非空则加入对应的clause。其他非主键字段将不会被用于查询clause
  3. clauseSelect.Columns:选择要查询的字段,按优先级从下面几种情况来选择:
    1. Statement.Selects为用户指定的select字段,如果有则加入clauseSelect.Columns。
    2. Statement.Schema存在且omit字段非空:从schema中获取字段,并排除 omits 中指定的字段
    3. 配置指定了 QueryFields 时,或者Dest的反射类型与Model指定的反射类型不一致,则表明存储目标对象Dest的结构与Model指定的结构存在不一致(通常为子集)。需要将Dest对象作为model解析一次,然后从其schema中提取所有的 DBNames 作为查询字段 clause.Columns。
  4. 对Join的处理。

执行查询

gorm.Scan 负责scan db结果到 statment.Dest 对象中。根据 Dest 类型的不同,其处理方式也存在差异。

map[string]interface{} 类型或者其指针、数组:

  • prepareValues:根据数据库表的模式信息或列类型信息,为每一列分配一个合适的、初始化为指针类型的值,以便后续能够存储从数据库查询结果中扫描Scan出来的数据。如果数据库表的模式信息或列类型信息中没有关于某一列的信息,则分配一个通用的interface{}指针。
  • scanIntoMap:将一个数据库查询结果行(values)映射到一个字符串键的映射(mapValue)中。如果mapValue[column]实现了driver.Valuer interface,则调用其Value方法获取值。如果mapValue[column]是sql.RawBytes,则将其转换为string。

reflect.Struct 类型或者其指针、数组

  • scanIntoStruct:将数据库查询结果 rows 扫描到结构体 reflectValue (即Dest)中。
    • field非空,则从field.NewValuePool内存池中获取一个空值。
    • scan到临时存储的字段值列表 values 中。
    • 没有嵌套字段,直接调用 field.setter 设置字段值
    • 如果有关联的JOIN字段,则处理嵌套结构:遍历嵌套JOIN路径(nestedJoinSchemas),逐级获取或创建嵌套结构体的反射值(relValue)。如果遇到指针类型的反射值且其指向的值为nil,则标记为isNilPtrValue并跳出循环。如果不是nil指针,则确保嵌套结构体存在,并在必要时创建它。最后,将值设置到最终的嵌套字段中。
    • 将values中的值放回field.NewValuePool中,以便重用。

 

查看全部文章查看全部文章