go-gorm源码分析(二)

October 28, 2024

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

ORM简介

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

典型用法:

[go]
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列表存在差异。

[go]
// 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列表。

[go]
// 设置写操作相关回调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链条的调用。

[go]
// 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列表。包括:

[go]
// 新增
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 链呢?

[go]
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。

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

[go]
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
[go]
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提供了两种方法可以指定模型:

[go]
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。二者间可以相互替换。

[go]
// 任意对象为空时使用另一个对象替换
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的定义如下:

[go]
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除外

数据类型包括:

[go]
const (
	Bool   DataType = "bool"
	Int    DataType = "int"
	Uint   DataType = "uint"
	Float  DataType = "float"
	String DataType = "string"
	Time   DataType = "time"
	Bytes  DataType = "bytes"
)

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

  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列表中。所以其解析逻辑基本也是围绕实现这一点而实现的。
[go]
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
}
[go]
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列表,与其他字段归属同一层级

[go]
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),用于通过反射写入新值到字段中 。

💡
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也提供了相关解决方法:

[go]
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 数组中。

[go]
// 设置嵌套字段的索引值。
// 因为嵌套字段会被添加到上层结构中,所以其原索引值需要追加其所属嵌套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 中设置字段的值等场景。如:

[go]
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中的其他信息。

[go]

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() 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

[go]
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实现的。

[go]
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存储所有关系

[go]
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做了什么:

[go]
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接口的主要逻辑,其主要内容如下:


  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。

    [go]
    type Values struct {
    	Columns []Column
    	Values  [][]interface{}
    }
    
    // Name from clause name
    func (Values) Name() string {
    	return "VALUES"
    }

    看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不准确的情况出现。
    [sql]
    // mysql 修改全局自增步长
    set global auto_increment_increment=8;
    
    // gorm中批量写入三条记录,gorm返回的id:252627
    // [2.294ms] [rows:3] INSERT INTO `t_product` (`created_at`,`updated_at`,`deleted_at`,`code`,`price`) VALUES (?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?)
    // 25
    // 26
    // 27
    
    // 实际数据 253341
    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的流程。

[javascript]
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
[go]
// 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

  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中,以便重用。

 

See all postsSee all posts