go-gorm源码分析(二)
October 28, 2024
前文分析了go-gorm的大致结构,包括初始化以、连接管理以及SQL构建等,本文分析gorm下通过ORM(对象关系映射)读写数据的基本原理及执行逻辑。
ORM简介
ORM(对象关系映射)是gorm提供的核心能力,实现了golang下数据类型和DB中数据的映射,开发人员可以通过操作对象的属性和方法,来间接操作数据库中的数据,而无需编写复杂的SQL语句。
典型用法:
type Product struct {
gorm.Model
Code string
Price uint
}
func main() {
// ...
// Create
db.Create(&Product{Code: "D42", Price: 100})
// Read
var product Product
db.First(&product, "code = ?", "D42") // find product with code D42
// Update - update product's price to 200
db.Model(&product).Update("Price", 200)
// Delete - delete product
db.Delete(&product, 1)
}
本文重点介绍创建(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列表存在差异。
// DB下的callbacks指针,实际为操作类型到callbacks的映射。
type callbacks struct {
processors map[string]*processor
}
// 在请求执行过程中,会根据 crud 的类型,从 callbacks 中获取对应类型的 processor.
func initializeCallbacks(db *DB) *callbacks {
return &callbacks{
processors: map[string]*processor{
"create": {db: db},
"query": {db: db},
"update": {db: db},
"delete": {db: db},
"row": {db: db},
"raw": {db: db},
},
}
}
type processor struct {
db *DB
// 拼接 sql 时的关键字顺序. 比如 query 类,固定为 SELECT,FROM,WHERE,GROUP BY, ORDER BY, LIMIT, FOR
// 在RegisterDefaultCallbacks 中指定
Clauses []string
// 对应于 crud 类型的执行函数链
fns []func(*DB)
callbacks []*callback
}
- Clauses 的作用前文已经介绍过了,此处不再重复。
- fns:处理后的callbacks列表,用于最终执行gorm主要逻辑。
- callbacks :原始注册的callbacks。
processor下提供了 Register、Rmove、Replace 几个API,用于挂载处理函数callback列表。在 diactor 的初始化过程中,会调用 callbacks.RegisterDefaultCallbacks(),注册各个processor所需的callbacks列表。
// 设置写操作相关回调callbacks 以及 配置
createCallback := db.Callback().Create()
// 开启事务
createCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
// 业务回调: BeforeSave、BeforeCreate
createCallback.Register("gorm:before_create", BeforeCreate)
// 处理插入前的关系:仅 BelongTo 关系
createCallback.Register("gorm:save_before_associations", SaveBeforeAssociations(true))
// 插入SQL构建及执行
createCallback.Register("gorm:create", Create(config))
// 查理插入后关系:包括 HasOne、HasMany、Many2Many 关系
createCallback.Register("gorm:save_after_associations", SaveAfterAssociations(true))
// 业务回调:AfterCreate、AfterSave
createCallback.Register("gorm:after_create", AfterCreate)
// 结束事务
createCallback.Match(enableTransaction).Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction)
// 设置dialector所需 Clause 名称
createCallback.Clauses = config.CreateClauses
可见写场景下DB的核心逻辑基本都在callbacks链表中覆盖到了,包括开启事务、处理前置关系、构建SQL及执行、处理后置关系以及结束事务等。其中事务处理、关系处理相关的回调函数会被update、delete等processor复用。
后续在 Finisher Method 被调用时,会触发API对应processor的Execute,在Execute中会触发callback链条的调用。
// Find API 中会使用 query 对应的callback列表来执行Execute
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)
}
// Create API 则会调用 create 对应的callback列表来执行Execute
func (db *DB) Create(value interface{}) (tx *DB) {
if db.CreateBatchSize > 0 {
return db.CreateInBatches(value, db.CreateBatchSize)
}
tx = db.getInstance()
tx.Statement.Dest = value
return tx.callbacks.Create().Execute(tx)
}
// 各API最终的执行逻辑都落到了这里
// Execute负责 callback 链条的调度
func (p *processor) Execute(db *DB) *DB {
// ...
// 遍历执行 fns 链
// 其中最核心的 create/query/update/delete 操作都被包含在其中了. 还包括了一系列前、后处理函数
for _, f := range p.fns {
f(db)
}
// ...
}
注册流程
分析注册callback的几个细节,Processor提供了几个API,用于操作callback列表。包括:
// 新增
func (c *callback) Register(name string, fn func(*DB)) error {
c.name = name
c.handler = fn
c.processor.callbacks = append(c.processor.callbacks, c)
return c.processor.compile()
}
// 删除
func (c *callback) Remove(name string) error {
c.processor.db.Logger.Warn(context.Background(), "removing callback `%s` from %s\n", name, utils.FileWithLineNum())
c.name = name
c.remove = true
c.processor.callbacks = append(c.processor.callbacks, c)
return c.processor.compile()
}
// 替换
func (c *callback) Replace(name string, fn func(*DB)) error {
c.processor.db.Logger.Info(context.Background(), "replacing callback `%s` from %s\n", name, utils.FileWithLineNum())
c.name = name
c.handler = fn
c.replace = true
c.processor.callbacks = append(c.processor.callbacks, c)
return c.processor.compile()
}
在 RegisterDefaultCallbacks 中只有Register的操作,可见gorm为各类dialector提供了一套默认的callback列表,同时也提供了修改、替换callback的能力,以适配各dialector的差异。
在Register、Remove、Replace操作中,只是向 []*callback 中追加了一条带标识(remove、replace)的记录,具体如何生效于 fns 链呢?
func (p *processor) compile() (err error) {
var callbacks []*callback
removedMap := map[string]bool{}
for _, callback := range p.callbacks {
// match通过才添加到callbacks,否则不添加。match为nil则添加到callbacks
// 比如事务回调,需要判断配置 SkipDefaultTransaction 是否开启
if callback.match == nil || callback.match(p.db) {
callbacks = append(callbacks, callback)
}
// 需要删除的回到调函数,需要记录下来,后面删除
if callback.remove {
removedMap[callback.name] = true
}
}
// 删除需要删除的回调函数,避免重复添加
if len(removedMap) > 0 {
callbacks = removeCallbacks(callbacks, removedMap)
}
p.callbacks = callbacks
// 排序回调函数
if p.fns, err = sortCallbacks(p.callbacks); err != nil {
p.db.Logger.Error(context.Background(), "Got error when compile callbacks, got %v", err)
}
return
}
添加到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。
一个典型的模型定义如下:
type User struct {
gorm.Model // 内嵌gorm.Model,以便使用其提供的字段和方法
Name string
Age sql.NullInt64
Birthday *time.Time
Email string `gorm:"type:varchar(100);uniqueIndex"`
Role string `gorm:"size:255"`
// 其他字段定义...
}
包括以下几部分信息:
- gorm.Model:内嵌模型,非必须。也可以自定义主键等。注意内嵌模型虽然在结构体内是作为struct的嵌套字段,但是gorm在DB操作上是作为其他字段的同级字段使用的。比如在查询的SQL语句应该是 select ID, CreateAt, Name, Age 而非 select model.ID, model.CreateAt, Name, Age 。
type Model struct {
ID uint `gorm:"primarykey"` // 默认主键
CreatedAt time.Time // 创建时间
UpdatedAt time.Time // 更新时间
DeletedAt DeletedAt `gorm:"index"` // 删除时间。用于soft delete
}
- 用户自定义字段和数据类型。默认情况下,每个字段对应DB表中的一个field,也可以通过tag过滤掉DB相关的映射关系。可以使用tag定义来跳过部分非db字段的映射关系。
- Tag:非必须。用于指定DB相关的约束信息,包括数据类型、大小、允许为空、PrimaryKey等信息。参考官方文档。
API提供了两种方法可以指定模型:
var user User
var users []User
// works because destination struct is passed in
db.First(&user)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1
// works because model is specified using `db.Model()`
result := map[string]interface{}{}
db.Model(&User{}).First(&result)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1
方法一:通过Model函数指定。
方法二:在Finisher Method中直接指定Dest对象,如果没有指定Model,则会直接使用Dest对象作为model。二者间可以相互替换。
// 任意对象为空时使用另一个对象替换
if stmt.Model == nil {
stmt.Model = stmt.Dest
} else if stmt.Dest == nil {
stmt.Dest = stmt.Model
}
// 解析 model,获取对应表的 schema 信息
if stmt.Model != nil {
if err := stmt.Parse(stmt.Model); err != nil && (!errors.Is(err, schema.ErrUnsupportedDataType) || (stmt.Table == "" && stmt.TableExpr == nil && stmt.SQL.Len() == 0)) {
if errors.Is(err, schema.ErrUnsupportedDataType) && stmt.Table == "" && stmt.TableExpr == nil {
db.AddError(fmt.Errorf("%w: Table not set, please set it like: db.Model(&user) or db.Table(\"users\")", err))
} else {
db.AddError(err)
}
}
}
在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的定义如下:
type Field struct {
// 字段名称
Name string
// column 标签指定 db 列名。如果未指定,则使用字段名称转换(见:namer.ColumnName)
DBName string
// 字段的绑定名称,默认为结构体中的字段名称,当为嵌套字段时,也会追加嵌套字段的名称。比如 ["Model", "ID"]
BindNames []string
// 与bindNames相同,不包括Anonymous struct
EmbeddedBindNames []string
// 字段数据类型,共6种:bool、int、uint、float、string、time、bytes。还有自定义 https://gorm.io/docs/data_types.html#GormDataTypeInterface
// 嵌套字段类型为空
DataType DataType
GORMDataType DataType
PrimaryKey bool // 是否为pk
AutoIncrement bool // 是否自增
AutoIncrementIncrement int64 // 自动步长,控制连续记录之间的间隔
// 权限控制,用于控制字段的读写权限
Creatable bool
Updatable bool
Readable bool
AutoCreateTime TimeType // 参考 tag: autoCreateTime
AutoUpdateTime TimeType
// 有默认值('Default')标识 或者 AUTOINCREMENT,则HasDefaultValue为true
HasDefaultValue bool
// 原始默认值 string
DefaultValue string
// defalut value值,使用interface{}类型,方便存储bool、int等类型
// 可以指定 Default tag标签,但是不指定默认值。比如 `gorm:"default:(-)"`. see https://gorm.io/docs/create.html#Default-Values
DefaultValueInterface interface{}
// TAG 定义的标识,参考文档tag值定义
NotNull bool
Unique bool
Comment string
Size int
Precision int
Scale int
IgnoreMigration bool
// 反射类型
FieldType reflect.Type
// 反射类型。如果字段类型为指针,则返回指向的类型,否则等同于FieldType
IndirectFieldType reflect.Type
// 原始结构体字段
StructField reflect.StructField
// 原标签信息
Tag reflect.StructTag
TagSettings map[string]string
// 指向所属的Schema:当为嵌套字段时,指向其所属的根Schema
Schema *Schema
// 当前fields中嵌套字段的schema。注意嵌套schema下的fields也会被加入到schema.Fields 中
EmbeddedSchema *Schema
// 当前字段所属的Schema
OwnerSchema *Schema
// 用来获取struct中的字段值(reflect.Value),用于通过反射设置新值。
// 类似于 reflect.Value.FieldByIndex,内部包含了指针的初始化
ReflectValueOf func(context.Context, reflect.Value) reflect.Value
// 与 ReflectValueOf 相同,但返回的是 interface{} 类型以及是否为零值(fieldValue.IsZero())
ValueOf func(context.Context, reflect.Value) (value interface{}, zero bool)
// 写字段值函数。在 scanIntoStruct 中调用到
Set func(context.Context, reflect.Value, interface{}) error
// SerializerInterface:如果字段实现了Serializer,则指向序列化接口。见 https://gorm.io/docs/serializer.html
Serializer SerializerInterface
// 为 IndirectFieldType 分配的缓存池,避免重复gc。在读取场景 scanToStruct 时,会使用 NewValuePool 分配临时值,避免gc
NewValuePool FieldNewValuePool
// In some db (e.g. MySQL), Unique and UniqueIndex are indistinguishable.
// When a column has a (not Mul) UniqueIndex, Migrator always reports its gorm.ColumnType is Unique.
// It causes field unnecessarily migration.
// Therefore, we need to record the UniqueIndex on this column (exclude Mul UniqueIndex) for MigrateColumnUnique.
UniqueIndex string
}
字段解析在 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除外
数据类型包括:
const (
Bool DataType = "bool"
Int DataType = "int"
Uint DataType = "uint"
Float DataType = "float"
String DataType = "string"
Time DataType = "time"
Bytes DataType = "bytes"
)
也可以自定义数据类型。数据类型有几种设置方法:
- gorm自动映射:gorm中会根据golang struct字段数据类型进行推论。比如 reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64 这些类型会映射为 Int 。
- 字段类型实现了 GormDataTypeInterface 接口,则使用接口返回值作为gorm数据类型。
- 使用 tag 指定类型名。
- 实现了Serializer接口的字段,字段类型会被强制为String。
- 如果为struct字段,且没有实现 GormDataTypeInterface
- 空权限字段,datatype为空
默认值:
- HasDefaultValue:是否有指定默认值标签(Default Tag)或者 AUTOINCREMENT,则HasDefaultValue为true。
- DefaultValueInterface:默认值interface指针,可能为bool、int等类型。可以指定 Default tag标签,但是不指定默认值。比如 gorm:"default:(-)"。如果设置 AUTOINCREMENT 也会将 HasDefaultValue 设置为true,而DefaultValueInterface值为空。
- DefaultValue:tag中指定的默认值string。
通常情况下 HasDefaultValue 和 DefaultValueInterface 是要么同时存在,要么同时不存在的。但是有些特殊场景在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。
type Author struct {
Name string
Email string
}
type Blog struct {
Author
ID int
Upvotes int32
}
// 上面的匿名结构体Blog等同于此结构
type Blog struct {
ID int64
Name string
Email string
Upvotes int32
}
type Author struct {
Name string
Email string
}
type Blog struct {
ID int
Author Author `gorm:"embedded"`
Upvotes int32
}
// 上面的embedded结构体等同于此结构
type Blog struct {
ID int64
Name string
Email string
Upvotes int32
}
匿名和embedded fields在被解析后会被加入到schema.fields列表,与其他非struct类型的字段同级。如果存在多级嵌套,会递归解析并加入到最外层struct的fields列表,与其他字段归属同一层级。
func (schema *Schema) ParseField(fieldStruct reflect.StructField) *Field {
// ...
// 递归解析 embedded 字段到当前结构的 EmbeddedSchema 中
if field.EmbeddedSchema, err = getOrParse(fieldValue.Interface(), cacheStore, embeddedNamer{Table: schema.Table, Namer: schema.namer}); err != nil {
schema.err = err
}
// ...
}
// 解析字段列表:将modelType中所有字段解析为Field,并加入到schema.Fields中。
for i := 0; i < modelType.NumField(); i++ {
if fieldStruct := modelType.Field(i); ast.IsExported(fieldStruct.Name) {
// 如果是嵌套字段,将其下所有递归解析出的字段加入到schema中。
if field := schema.ParseField(fieldStruct); field.EmbeddedSchema != nil {
schema.Fields = append(schema.Fields, field.EmbeddedSchema.Fields...)
} else {
// 否则将该字段加入到schema.Fields中
schema.Fields = append(schema.Fields, field)
}
}
}
值读写相关的函数(ValuerAndSetter)
- ReflectValueOf
用来获取struct中的字段值(reflect.Value),用于通过反射写入新值到字段中 。
- 获取字段reflect.Value。在struct中只能通过 reflect.Indirect(value).Field(index) 来或者 reflect.Value对象。
- 调用reflect.Value对象的 SetXXX 方法。比如 SetInt、SetBool 等。或者直接调用 Set(需要校验类型可转换ConvertibleTo)
由于Fields列表中存在嵌套和匿名struct字段,所以不能直接使用 refelct.Field[index] 来获取reflect.Value对象,否则获取到的可能是嵌套struct字段。所以golang也提供了相关解决方法:
type user struct {
firstName string
lastName string
}
type data struct {
user
firstName string
lastName string
}
u := data{
user: user{"Embedded John", "Embedded Doe"},
firstName: "John",
lastName: "Doe",
}
s := reflect.ValueOf(u).FieldByIndex([]int{0, 1})
fmt.Println("embedded last name:", s)
对于嵌套struct,需要调用 value.FieldByIndex 并告知嵌套字段的路由信息。在FieldByIndex中会根据路由自顶向下的解析各struct,获取到最底层的基础类型字段。
但FieldByIndex有个问题,如果嵌套结构体为空指针,则会抛出异常(因为未分配内存)。FieldByIndex中不会主动初始化指针对象。所以gorm实现了自己的FieldByIndex,即 field.ReflectValueOf 方法,内部包含了(空)指针的处理逻辑。字段路由信息保存在 field.StructField.Index 数组中。
// 设置嵌套字段的索引值。
// 因为嵌套字段会被添加到上层结构中,所以其原索引值需要追加其所属嵌套struct字段的索引值。
// index is negative means is pointer
if field.FieldType.Kind() == reflect.Struct {
ef.StructField.Index = append([]int{fieldStruct.Index[0]}, ef.StructField.Index...)
} else {
// 如果对象为指针,则用负数index表示
ef.StructField.Index = append([]int{-fieldStruct.Index[0] - 1}, ef.StructField.Index...)
}
// struct 嵌套或者embedded字段的反射值获取
field.ReflectValueOf = func(ctx context.Context, v reflect.Value) reflect.Value {
v = reflect.Indirect(v)
// 自顶向下获取嵌套字段的反射值
for idx, fieldIdx := range field.StructField.Index {
if fieldIdx >= 0 {
v = v.Field(fieldIdx)
} else {
// 指针类型
v = v.Field(-fieldIdx - 1)
if v.IsNil() {
// 初始化指针对象
v.Set(reflect.New(v.Type().Elem()))
}
if idx < len(field.StructField.Index)-1 {
v = v.Elem()
}
}
}
return v
}
主要作用:ReflectValueOf的作用类型与golang中 reflect.Field.Index() ,用于赋值函数 fields.Set 中设置字段的值等场景。如:
switch field.FieldType.Kind() {
// bool类型字段的赋值函数
case reflect.Bool:
field.Set = func(ctx context.Context, value reflect.Value, v interface{}) error {
switch data := v.(type) {
case **bool:
if data != nil && *data != nil {
field.ReflectValueOf(ctx, value).SetBool(**data)
}
case bool:
field.ReflectValueOf(ctx, value).SetBool(data)
case int64:
field.ReflectValueOf(ctx, value).SetBool(data > 0)
case string:
b, _ := strconv.ParseBool(data)
field.ReflectValueOf(ctx, value).SetBool(b)
default:
return fallbackSetter(ctx, value, v, field.Set)
}
return nil
}
//...
}
- 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中的其他信息。
type Schema struct {
// modelType.Name()
Name string
// 反射type
ModelType reflect.Type
// 表明
Table string
PrioritizedPrimaryField *Field
// fields中的所有字段名称
DBNames []string
// 包含了primary key列表
PrimaryFields []*Field
PrimaryFieldDBNames []string
Fields []*Field // ParseField 解析出的struct字段
// Field.Name -> Field 的映射
FieldsByName map[string]*Field
// Field.BindName -> Field 的映射. 嵌套字段的 BindName 格式为 'Embed.Field', like `Model.ID`
FieldsByBindName map[string]*Field // embedded fields is 'Embed.Field'
// Field.DBName -> Field 的映射
FieldsByDBName map[string]*Field
// 字段默认值依赖DB的默认值生成,tag中仅设置默认值表示,不提供默认值实现。
FieldsWithDefaultDBValue []*Field // fields with default value assigned by database
Relationships Relationships
// 各字段的默认clause,会被加入到所有会话中。比如 DeleteAt 字段的软删除条件
CreateClauses []clause.Interface
QueryClauses []clause.Interface
UpdateClauses []clause.Interface
DeleteClauses []clause.Interface
BeforeCreate, AfterCreate bool
BeforeUpdate, AfterUpdate bool
BeforeDelete, AfterDelete bool
BeforeSave, AfterSave bool
AfterFind bool
err error
// 初始化标识,避免多goroutine并行解析导致的冲突
initialized chan struct{}
// DB.NamingStrategy
namer Namer
// 对应cacheStore
cacheStore *sync.Map
}
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() inerface 、 TablerWithNamer interface 、 embeddedNamer
- cacheStore:指向DB对象的cacheStore。用于缓存schema,避免重复解析。其key值默认为 modelType 。即每个struct类型只会被解析一次(性能优化)。
字段(field)相关:
- Fields:struct下所有的字段列表。
字段可以分类几类:
- go基础类型字段:比如string、int等类型的field
- 匿名、embedded类型字段:此类字段本身不会加入字段列表,而是将struct下的所有字段加入到fields列表。
- 普通struct类型字段:通用用于存储关系数据。此类字段会作为一个单独字段加入字段列表。
- 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
BeforeCreate, AfterCreate bool
BeforeUpdate, AfterUpdate bool
BeforeDelete, AfterDelete bool
BeforeSave, AfterSave bool
AfterFind bool
Clause:
可以给struct中的各字段自定义各种clause,这些clause会被作为公共clause追加到该model所有的CRUD的sql中。比如软删除的clause就是通过在 gorm.Model.DeleteAt字段中添加了各种cluase实现的。
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}
// 软删除的queryCluase、DeleteCluase等
func (DeletedAt) QueryClauses(f *schema.Field) []clause.Interface {
return []clause.Interface{SoftDeleteQueryClause{Field: f, ZeroValue: parseZeroValueTag(f)}}
}
func (DeletedAt) DeleteClauses(f *schema.Field) []clause.Interface {
return []clause.Interface{SoftDeleteDeleteClause{Field: f, ZeroValue: parseZeroValueTag(f)}}
}
func (DeletedAt) UpdateClauses(f *schema.Field) []clause.Interface {
return []clause.Interface{SoftDeleteUpdateClause{Field: f, ZeroValue: parseZeroValueTag(f)}}
}
关系:
RelationShips存储所有关系
type Relationships struct {
HasOne []*Relationship
BelongsTo []*Relationship
HasMany []*Relationship
Many2Many []*Relationship
Relations map[string]*Relationship
EmbeddedRelations map[string]*Relationships
}
关系部分后面再详细分析。
其他:
注意在解析schema时有对cacheStore多次Load、LoadOrStore,是为了考虑多goroutine并行的情况。
Create流程
解析关系分析完毕后,再分析Create执行的流程。先看API做了什么:
func (db *DB) Create(value interface{}) (tx *DB) {
if db.CreateBatchSize > 0 {
return db.CreateInBatches(value, db.CreateBatchSize)
}
tx = db.getInstance()
tx.Statement.Dest = value
return tx.callbacks.Create().Execute(tx)
}
仅设置了目标对象值 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接口的主要逻辑,其主要内容如下:
- DB异常检测
如果 db.Error 不为 nil,则直接返回,不执行任何操作。
因为processor的callback链条中间不会退出,所以需要提前对error进行检查。如果db有异常,不再进行插入处理。执行下一个callback即可。
- schema公共clause处理
schema公共clause中的CreateClauses,会被添加到本次create操作中(如果有)。
ps:软删除字段DeleteTime没有对create场景进行特殊处理,因为默认为空。而CreatedAt、UpdatedAt会在ParseField中打上AutoUpdateTime、AutoCreateTime的标识。
- Returning子句的处理
如果DB支持RETURNING(参考 go-gorm/mysql 中的CreateClauses )即表明需要在 insert 语句之后插入 RETURNING子句,以获取DB写入的字段内容。
注意RETURNING字段的选择,仅选择 FieldsWithDefaultDBValue 字段,即依赖DB默认值的字段。其他类型的字段gorm自己可以生成(所以结构体默认值的字段定义需要与DB中的保持一致)。
- ConvertToCreateValues:将dest转换为clause条件
构建插入SQL之前还需要构建 clause.Values ,此clause的主要目的是生成SQL中的 (field1, field2) valuels (?, ?) 部分,ConvertToCreateValues的主要目的就是根据传入的Dest结构生成对应的clause。
[go]type Values struct { Columns []Column Values [][]interface{} } // Name from clause name func (Values) Name() string { return "VALUES" }看value clause,其核心结构由两部分组成:字段名列表以及对应的插入值列表(二维数组以支持批量插入)。所以ConvertToCreateValues的逻辑的重点也是在于生成此两个字段列表。后面详细展开。
- Build SQL
Statement.Build 构建插入SQL。参考之前的SQL构建流程。构建完毕的sql存储在 statement.SQL 中。
- 执行SQL
如果配置了 RETURNING 子句并且支持(supportReturning 为 true),则使用 db.Statement.ConnPool.QueryContext 执行查询并处理返回的行。
否则,使用 db.Statement.ConnPool.ExecContext 执行 SQL 语句
- 处理插入结果
如果支持 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类型的处理:
- SelectAndOmitColumns:获取当前查询(或插入)操作中要包含的列(selectColumns)和不允许的列(restricted)。
- 排序key:取出mapValue中所有的keys,并排序。排序是为了确保多次生成的SQL语句中的列顺序是一致的,之前也介绍过连接池会根据sql缓存prepare结果。
- 遍历键并构建clause.Values ,插入到clause.Values
- Column:对应字段对象 clause.Column。默认为key名称,如果有 schema,则会根据key名称查找field,并使用 field.DBName 。
- Value:map数据的value值,不做任何处理。
对于map的扩展类型( *map[string]interface{} []map[string]interface{} *[]map[string]interface{} )都是类似的。
struct类型的处理:
- SelectAndOmitColumns:获取当前查询(或插入)操作中要包含的列(selectColumns)和不允许的列(restricted)。
- 设置字段对象 values.Columns 和值对象 values.Values ,分两种情况:
- 需要gorm提供默认值的字段:这里包括了使用Tag定义了明确DefaultValue的字段 和 没有指定默认Tag(HasDefaultValue为false)的字段。这类字段在写入db的value选择时优先使用struct field中的原值(通过 field.ValueOf 获取字段值)。单如果用户为设置其值(isZero为空),则使用 DefaultValueInterface 生成默认值 或者 当前时间填充带有 AutoCreateTime、AutoUpdateTime标识的字段。
- 字段在FieldsWithDefaultDBValue 中,此类型的字段表示DB会提供默认值,不需要gorm提供。在插入时需要跳过此字段,除非struct中的字段值非空,表示需要插入用户指定的明确值。
最后,还需要对 ON CONFLICT 进行处理,如果有 clause.OnConflict 条件,且开启了 UpdateAll,需要对 onConflict 进行处理,即插入的字段也需要追加对应的 DO UPDATE SET 子句。
注意点:
- 非RETURNING的场景(比如mysql)auto_increment的处理,批量插入时,gorm中是使用lastID+自增步长的方式本地计算的。如果DB有修改自增步长,而tag没有指定准确的值,就会导致批量插入时id不准确的情况出现。[sql]
// mysql 修改全局自增步长 set global auto_increment_increment=8; // gorm中批量写入三条记录,gorm返回的id:25、26、27 // [2.294ms] [rows:3] INSERT INTO `t_product` (`created_at`,`updated_at`,`deleted_at`,`code`,`price`) VALUES (?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?) // 25 // 26 // 27 // 实际数据 25、33、41 ysql> select * from t_product where id >=25; +----+-------------------------+-------------------------+------------+--------+-------+ | id | created_at | updated_at | deleted_at | code | price | +----+-------------------------+-------------------------+------------+--------+-------+ | 25 | 2024-10-23 23:10:49.002 | 2024-10-23 23:10:49.002 | NULL | batch1 | 1 | | 33 | 2024-10-23 23:10:49.002 | 2024-10-23 23:10:49.002 | NULL | batch2 | 1 | | 41 | 2024-10-23 23:10:49.002 | 2024-10-23 23:10:49.002 | NULL | batch3 | 1 | +----+-------------------------+-------------------------+------------+--------+-------+
Query流程
以 Find API为例,来看看query的流程。
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)
}
API跟上面的逻辑基本一致,仅设置 Dest 对象,用来存放查询结果。与Create一样,Dest也支持map和struct以及对应指针、数组。
Query的processor有个三个callbak:
- Query:主逻辑,执行查询
- Preload:执行Preload逻辑,为何预加载在Query之后?
- AfterQuery:执行 AfterFind hook
// doesn't work
result := map[string]interface{}{}
db.Table("users").First(&result)
// works with Take
result := map[string]interface{}{}
db.Table("users").Take(&result)
Query callback中分位两步:构建SQL(BuildQuerySQL) 以及 执行(gorm.Scan)。
BuildQuerySQL
- Schema.QueryClauses 追加到本次事务的clause中。当前主要是加入软删除相关的字段。然后开始SQL的构建。
- PrimaryFields 字段处理:根据schema中的Schema.PrimaryFields列表,检查struct中对应的字段是否为zero。如果非空则加入对应的clause。其他非主键字段将不会被用于查询clause。
- clauseSelect.Columns:选择要查询的字段,按优先级从下面几种情况来选择:
- Statement.Selects为用户指定的select字段,如果有则加入clauseSelect.Columns。
- Statement.Schema存在且omit字段非空:从schema中获取字段,并排除 omits 中指定的字段
- 配置指定了 QueryFields 时,或者Dest的反射类型与Model指定的反射类型不一致,则表明存储目标对象Dest的结构与Model指定的结构存在不一致(通常为子集)。需要将Dest对象作为model解析一次,然后从其schema中提取所有的 DBNames 作为查询字段 clause.Columns。
- 对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中,以便重用。