基于 GORM 的插件原理解析和开发实战

开篇

繁重的历史技术债务经常让我们头疼。业务的领域模型能否有效地抽象出来,以及系统的基座是否是合理尤为重要。一个可扩展,插件化的系统能够帮助我们解耦很多不必要的依赖,低成本地支持更多的功能。

业务模型的抽象很大程度上,取决于做事的人(包括PM, RD 相关的 owner)对于业务的理解是否充分,能够区分出来什么应该支持,什么时候要 say no,以及需求的理想解法。这一点并不容易,难就难在于不仅仅要想清楚,还要达成共识。

假定对于业务模型的理解是充分的,作为研发,我们怎样设计整个系统才能让它保持敏捷,便于扩展,每一次改动都能控制在与需求规模基本匹配的范围内,而不是每次都要动【基座】,这一点非常重要。

个人经验来看,要设计出一个理想的基座,最好的办法还是多看优秀的框架,优秀的开源代码。

因为要通用,所以在开源项目的领域内,大佬们不会写出非常局限于某个业务的逻辑,对于扩展能力考虑的也会更充分。多学习开源项目的扩展,插件设计,对于个人成长是非常有用的。

今天我们来看看 GORM 的插件化设计是怎样的,以及怎样开发一个对业务有用的插件。

插件设计

代码仓库:github.com/go-gorm/gor…

interfaces.go 中,我们可以找到插件的接口定义:

type Plugin interface {
    Name() string
    Initialize(*DB) error
}
复制代码

这一节我们通过源码先来看看插件是怎么生效的。

type Config struct {
	...
	// Plugins registered plugins
	Plugins map[string]Plugin

	callbacks  *callbacks
	cacheStore *sync.Map
}

// AfterInitialize initialize plugins after db connected
func (c *Config) AfterInitialize(db *DB) error {
	if db != nil {
		for _, plugin := range c.Plugins {
			if err := plugin.Initialize(db); err != nil {
				return err
			}
		}
	}
	return nil
}
复制代码

首先,在 gorm.Config 中我们可以找到 Plugin 类型的字段,定义了一个 Plugins 字段,对应了一个插件的 map。Config 对象支持 AfterInitialize 操作,根据注释可以知道,这是 db 链接之后调用插件的 Initialize 方法,对每个插件进行初始化。


// Open initialize db session based on dialector
func Open(dialector Dialector, opts ...Option) (db *DB, err error) {
	config := &Config{}
        
        ...
        
	if config.Plugins == nil {
		config.Plugins = map[string]Plugin{}
	}

	...

	db = &DB{Config: config, clone: 1}
	db.callbacks = initializeCallbacks(db)
	db.Statement = &Statement{
		DB:       db,
		ConnPool: db.ConnPool,
		Context:  context.Background(),
		Clauses:  map[string]clause.Clause{},
	}
	return
}
复制代码

在我们通过 Open 拿到一个 gorm.DB 对象时,会初始化一个空的 map[string]Plugin{},并把对应的 Config 赋值给 DB 对象。

// Use use plugin
func (db *DB) Use(plugin Plugin) error {
	name := plugin.Name()
	if _, ok := db.Plugins[name]; ok {
		return ErrRegistered
	}
	if err := plugin.Initialize(db); err != nil {
		return err
	}
	db.Plugins[name] = plugin
	return nil
}
复制代码

DB 对象可以接受一个 Plugin,这个时候会去完成注册,并调用 Initialize 方法对传入的插件执行初始化。

注:实际上对照源码看了一遍,似乎没有发现调用 Config 的 AfterInitialize 方法的地方。目前看起来还是需要依赖这个 DB.Use 方法来实现将 plugin 注册到 DB 对象中并初始化。(如果理解有错欢迎评论指正)

GORM provides a Use method to register plugins, the plugin needs to implement the Plugin interface. The Initialize method will be invoked when registering the plugin into GORM first time, and GORM will save the registered plugins.

Callback

看到这里,我们会发现,其实本质上 GORM 对 plugin 仅仅是暴露了一个 Initialize 这样的初始化方法。这里就很有意思了,作为使用 GORM 的业务方,我需要在一个初始化方法里表达插件逻辑,那么当我执行 CRUD 操作的时候,GORM 是怎么感知到的呢?

这里其实就是用到了 GORM 设计中经典的 Callback 能力。我们来看看官方文档的描述:

GORM itself is powered by Callbacks, it has callbacks for CreateQueryUpdateDeleteRowRaw, you could fully customize GORM with them as you want

Callbacks are registered into the global *gorm.DB, not the session-level, if you require *gorm.DB with different callbacks, you need to initialize another *gorm.DB

那么 Callback 到底是什么呢?

我们来回顾一下,上一节看到 Config 的时候其实就有过:

type Config struct {
	...
	// Plugins registered plugins
	Plugins map[string]Plugin

	callbacks  *callbacks
	cacheStore *sync.Map
}

// callbacks gorm callbacks manager
type callbacks struct {
	processors map[string]*processor
}

type processor struct {
	db        *DB
	Clauses   []string
	fns       []func(*DB)
	callbacks []*callback
}

type callback struct {
	name      string
	before    string
	after     string
	remove    bool
	replace   bool
	match     func(*DB) bool
	handler   func(*DB)
	processor *processor
}

复制代码

每一个 DB 对象对应一个 callbacks map,其中注册了一堆 processor,每个 processor 又对应了一组 callback 对象,包含了 before, after。

这里可能还不太能看出来原理,不要着急,我们看看,如果我是一个纯新的 gorm.DB 对象,默认的 callbacks 属性是什么呢?

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},
		},
	}
}

func (cs *callbacks) Create() *processor {
	return cs.processors["create"]
}

func (cs *callbacks) Query() *processor {
	return cs.processors["query"]
}

func (cs *callbacks) Update() *processor {
	return cs.processors["update"]
}

func (cs *callbacks) Delete() *processor {
	return cs.processors["delete"]
}

func (cs *callbacks) Row() *processor {
	return cs.processors["row"]
}

func (cs *callbacks) Raw() *processor {
	return cs.processors["raw"]
}
复制代码

发现了么?初始化的时候,其实就是 CRUD + row + raw 一共六个指令下,注册自己的 processor,没有各种钩子,只是简单的把 gorm.DB 对象传进去。

而我们可以通过 Create(), Query(), Update() 等方法,获取对应的 processor (里面包含了实际加入进来的 callback 对象),这里的实现,本质上也就是从 map 里取值而已。

至此,简单总结一下涉及到的几个概念的定位。

  • callbacks:gorm Config 中的成员变量,代表了注册到这个 DB 对象的所有 callback 信息,本质上是一个 manager 的角色,所有跟 callback 相关的操作封装到这里;
  • processor: 包含一个 gorm 操作所需要的直接依赖(DB 对象,语句,回调函数等),我们由此就可以去构造出来 sql 语句。上一点 callbacks 中包含了一个 map[string]*processor,key 代表了具体的某个操作(CRUD + row + raw),value 就是一个 processor;
  • callback:一个具体的回调函数相关元信息。

那么当我们去执行一次 GORM 方法调用(比如可能是 CRUD 任何一种),是怎么利用了 Callback 这套机制的呢?

其实很简单,我们来看一下 finisher_api.go 里面的实现:

  • Create
// Create insert the value into database
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)
}
复制代码
  • Find
// Find find records that match given conditions
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)
}
复制代码
  • Updates
// Updates update attributes with callbacks, refer: https://gorm.io/docs/update.html#Update-Changed-Fields
func (db *DB) Updates(values interface{}) (tx *DB) {
	tx = db.getInstance()
	tx.Statement.Dest = values
	return tx.callbacks.Update().Execute(tx)
}
复制代码
  • Delete
// Delete delete value match given conditions, if the value has primary key, then will including the primary key as condition
func (db *DB) Delete(value 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 = value
	return tx.callbacks.Delete().Execute(tx)
}
复制代码

发现了么?

全部都是调用 DB 对象的 callbacks 变量,拿到对应操作的 processor,去执行 Execute 方法。无论你是 CRUD 哪一种,本质都是一样的。其他的 finisher_api 只是中间多了一些处理步骤。本质上都是一样:

tx.callbacks.XXX().Execute(tx)
复制代码

这里可以看出来,GORM 本身是强依赖 Callback 这一套体系的,我们不管期望做什么操作,最后都会调用 某个 processor 的 Execute 方法。

既然焦点汇聚到了这里,我们来看看 processor 的 Execute 到底干了什么事。

func (p *processor) Execute(db *DB) *DB {
	...
	// parse model values
	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)
			}
		}
	}

	// assign stmt.ReflectValue
	if stmt.Dest != nil {
		stmt.ReflectValue = reflect.ValueOf(stmt.Dest)
		for stmt.ReflectValue.Kind() == reflect.Ptr {
			if stmt.ReflectValue.IsNil() && stmt.ReflectValue.CanAddr() {
				stmt.ReflectValue.Set(reflect.New(stmt.ReflectValue.Type().Elem()))
			}

			stmt.ReflectValue = stmt.ReflectValue.Elem()
		}
		if !stmt.ReflectValue.IsValid() {
			db.AddError(ErrInvalidValue)
		}
	}

	for _, f := range p.fns {
		f(db)
	}
        ...

	return db
}
复制代码

上面的代码是简化版,我们直接看关键。processor 的 Execute 方法本质上就干了三件事:

  1. 解析 model。还记得么?我们需要定义一个 gorm 的 struct,对应到表结构,这里就是去解析传入的对象,拿到字段类型,解析 tag;
  2. 根据传入的 Dest 去给 model 赋值,还是依靠反射完成的;
  3. 获取 processor 中注册的 fns 函数数组,依次 call 每个函数。

前两步问题不大,建议对反射不熟悉的同学多看看,收获肯定不小。我们重点看第三个。

type processor struct {
	db        *DB
	Clauses   []string
	fns       []func(*DB)
	callbacks []*callback
}

for _, f := range p.fns {
        f(db)
}
复制代码

我们把两段代码放到一起,问题就来了,前一步我们大概能猜出来 []*callback 应该就是我们注册进去的各种回调函数。那这个 fns 看起来也挺像,这个是哪儿来的呢?

其实二者本质是一个东西,fns 可以认为是最终处理结果。

事实上,即便不谈开发者自定义 callback 和插件,GORM 自己去执行最基本的 CRUD 操作,也是通过 Callback 这一套机制来完成的。再加上开发者定义的 callback,会有大量的回调函数可能需要执行。

问题来了,各个 callback 之间,可能是存在相互依赖关系的,这个在 插件化设计中也是很常见的。比如 callbackA 中的逻辑希望放到 callbackB 之后执行。仅仅靠一个数组当然是不够的。

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 (p *processor) compile() (err error) {
	var callbacks []*callback
	for _, callback := range p.callbacks {
		if callback.match == nil || callback.match(p.db) {
			callbacks = append(callbacks, callback)
		}
	}
	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
}
复制代码

我们看到,实际去 Register 一个回调函数时,是直接 append 进数组,同时触发一个 compile 操作。这个 compile 就是会遍历整个数组,根据 before, after 那些属性,理清楚各个 callback 的最终执行顺序,赋值给 fns 变量。


func (c *callback) Before(name string) *callback {
	c.before = name
	return c
}

func (c *callback) After(name string) *callback {
	c.after = name
	return c
}

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()
}
复制代码

如果我们希望某个 callback 在另一个之前执行,只需要用这里的接口,声明一下 Before 和 After 即可,如果想要删除某个 callback 或替换,则可以用 Remove 和 Replace,成本很低。GORM 会在我们 Register的时候通过 compile 来算出来最终的顺序。计算的方法在 sortCallbacks 函数中,这里不再进一步展开,大家如果感兴趣可以直接看看源码。

好,Callback 的底层实现我们搞清楚了,那么作为调用方怎么用,有没有示例呢?GORM 默认注册的 Callback 就是最好的示范。我们来看一下:


func RegisterDefaultCallbacks(db *gorm.DB, config *Config) {
	...

	createCallback := db.Callback().Create()
	createCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
	createCallback.Register("gorm:before_create", BeforeCreate)
	createCallback.Register("gorm:save_before_associations", SaveBeforeAssociations(true))
	createCallback.Register("gorm:create", Create(config))
	createCallback.Register("gorm:save_after_associations", SaveAfterAssociations(true))
	createCallback.Register("gorm:after_create", AfterCreate)
	createCallback.Match(enableTransaction).Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction)
	createCallback.Clauses = config.CreateClauses

	queryCallback := db.Callback().Query()
	queryCallback.Register("gorm:query", Query)
	queryCallback.Register("gorm:preload", Preload)
	queryCallback.Register("gorm:after_query", AfterQuery)
	queryCallback.Clauses = config.QueryClauses

	deleteCallback := db.Callback().Delete()
	deleteCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
	deleteCallback.Register("gorm:before_delete", BeforeDelete)
	deleteCallback.Register("gorm:delete_before_associations", DeleteBeforeAssociations)
	deleteCallback.Register("gorm:delete", Delete(config))
	deleteCallback.Register("gorm:after_delete", AfterDelete)
	deleteCallback.Match(enableTransaction).Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction)
	deleteCallback.Clauses = config.DeleteClauses

	updateCallback := db.Callback().Update()
	updateCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
	updateCallback.Register("gorm:setup_reflect_value", SetupUpdateReflectValue)
	updateCallback.Register("gorm:before_update", BeforeUpdate)
	updateCallback.Register("gorm:save_before_associations", SaveBeforeAssociations(false))
	updateCallback.Register("gorm:update", Update(config))
	updateCallback.Register("gorm:save_after_associations", SaveAfterAssociations(false))
	updateCallback.Register("gorm:after_update", AfterUpdate)
	updateCallback.Match(enableTransaction).Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction)
	updateCallback.Clauses = config.UpdateClauses

	rowCallback := db.Callback().Row()
	rowCallback.Register("gorm:row", RowQuery)
	rowCallback.Clauses = config.QueryClauses

	rawCallback := db.Callback().Raw()
	rawCallback.Register("gorm:raw", RawExec)
	rawCallback.Clauses = config.QueryClauses
}
复制代码

虽然行数不少,但是一眼看过去就能大体明白,这里就是把6个操作的前前后后都加上 callback,本质上用的就是processor 对外暴露的 Register 方法。

func (*gorm.processor).Register(name string, fn func(*gorm.DB)) error
复制代码

比如我现在有一个 Create 的场景,就可以通过这种方式注册 callback:

createCallback := db.Callback().Create()
createCallback.Register("gorm:before_create", BeforeCreate)
复制代码

另外观察源码我们也可以发现,DB 的 Config 中其实包含一个 SkipDefaultTransaction 的 bool 配置。 GORM 自身是会默认将所有操作都放到【事务】中执行的,但如果你通过 Config 配置了把它 skip 掉,GORM 也会感知到,进而不去注册对应的 callback。

// 感知配置
enableTransaction := func(db *gorm.DB) bool {
        return !db.SkipDefaultTransaction
}
createCallback := db.Callback().Create()
// 只有在 enableTransaction 时才去注册 callback。
createCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
...
createCallback.Match(enableTransaction).Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction)
复制代码

ok,了解完 Callback,逻辑就通了。虽然我们编写插件只有一个 Initialize 的 API 可以利用,但是因为 gorm 提供了完备的 Callback 体系,我们如果想做什么,直接在对应的操作前后注册自定义的 Callback 即可。

Plugin 很大程度上是依赖 Callback 实现的,所以记得去看看 default 的 callback 名称,比如我希望在 Query 完成后做一些操作。在默认的 callback 中看到这几个。

queryCallback := db.Callback().Query()
queryCallback.Register("gorm:query", Query)
queryCallback.Register("gorm:preload", Preload)
queryCallback.Register("gorm:after_query", AfterQuery)
queryCallback.Clauses = config.QueryClauses
复制代码

那么就直接注册一个 After gorm:query 的回调函数即可。后面实战场景我们会详细说。

实战场景

判断查询是否命中索引

image.png

我们可以构建一个索引结果的结构体,在执行完真正 SQL 之后执行一次 Explain 看看

type ExplainResult struct {
	Id           int64   `gorm:"column:id"`
	SelectType   string  `gorm:"column:select_type"`   // 查询行为类型 simple primary union...
	Table        string  `gorm:"column:table"`         // tableName
	Partitions   string  `gorm:"column:partitions"`    // 分区
	Type         string  `gorm:"column:type"`          // 引擎层查询数据行为类型 system const ref index index_merge all ...
	PossibleKeys string  `gorm:"column:possible_keys"` // 可能用到的所有索引
	Key          string  `gorm:"column:key"`           // 真正用到的所有索引
	KeyLen       int32   `gorm:"column:key_len"`       // 查询时用到的索引长度
	Ref          string  `gorm:"column:ref"`           // 哪些列或常量与key所使用的字段进行比较
	Rows         int32   `gorm:"column:rows"`          // 预估需要扫描的行数
	Filtered     float32 `gorm:"column:filtered"`      // 根据条件过滤后剩余的行数百分比(预估)
	Extra        string  `gorm:"column:extra"`
}

type ExplainPlugin struct{}

func NewExplainPlugin() *ExplainPlugin {
  return &ExplainPlugin{}
}

func (e *ExplainPlugin) Name() string {
  return "explain_check"
}

func (e *ExplainPlugin) Initialize(db *gorm.DB) error {
  return db.Callback().Query().After("gorm:query").Register("explain_check", checkIndex)
}

func checkIndex(db *gorm.DB) {
  result := &ExplainResult{}
  session := &gorm.Session{
    NewDB: true, 
    Context: db.Statement.Context,
  }
  err := db.Session(session).Raw("EXPLAIN " + db.Statement.SQL.String(), db.Statement.Vars...).Scan(result).Error
  if err != nil {
    return
  }

  // 命中索引
  if r.Key != "" {
    fmt.Printf("hits index: %s", result.Key)
  }
}
复制代码

多租户隔离

全链路加 tenant 判断有时候很麻烦,放到 plugin 里代码会清爽一些,在 gorm:query 的回调之前执行。


type tenantPlugin struct{}

func (t *tenantPlugin) Name() string {
	return "tenant_plugin"
}

func (t *tenantPlugin) Initialize(db *gorm.DB) error {
	return db.Callback().Query().Before("gorm:query").Register("tenant_id:before_query", t.beforeQuery)
}

func (t *tenantPlugin) beforeQuery(db *gorm.DB) {
        // 一些业务逻辑,拿到 tenantID,可能从 context 中
        tenantID := XXXLogic(db.Statement.Context)
	if tenantID != "" {
		db.Statement.AddClause(clause.Where{Exprs: []clause.Expression{
			clause.Eq{Column: clause.Column{Table: db.Statement.Table, Name: "tenant_id"}, Value: tenantID},
		}})
	}
}

复制代码

实现写数据的 validator

很多时候我们需要在数据写入数据库前,校验数据格式,这个时候就可以用插件实现:

type validatorPlugin struct {
}

func (plugin *validatorPlugin) Name() string {
	return "validator"
}

func (plugin *validatorPlugin) Initialize(db *gorm.DB) error {
	validator := func(data interface{}) error {
                // 这里可以断言成业务对象,或某个 interface 实现
		if m, ok := data.(XXXXValidator); ok {
			if err := m.IsValid(); err != nil {
				return err
			}
		}
		return nil
	}
	db.Callback().Create().Before("gorm:create").Register("validator", func(d *gorm.DB) {
                // 这里有需要的话还可以根据 Statement.Model 的 kind进一步拆分处理
		err = validator(d.Statement.Model)
		if err != nil {
			d.AddError(err)
		}
	})
	return nil
}
复制代码

链路耗时记录

参考:segmentfault.com/a/119000003…

before 埋下时间戳,after 读出来,比较耗时即可。


const (
	callBackBeforeName = "biz:before"
	callBackAfterName  = "biz:after"
	startTime          = "_start_time"
)

type TracPlugin struct{}

func (op *TracPlugin) Name() string {
	return "tracePlugin"
}

func (op *TracPlugin) Initialize(db *gorm.DB) (err error) {
	_ = db.Callback().Create().Before("gorm:before_create").Register(callBackBeforeName)
	_ = db.Callback().Query().Before("gorm:query").Register(callBackBeforeName, before)
	_ = db.Callback().Delete().Before("gorm:before_delete").Register(callBackBeforeName, before)
	_ = db.Callback().Update().Before("gorm:setup_reflect_value").Register(callBackBeforeName, before)
	_ = db.Callback().Row().Before("gorm:row").Register(callBackBeforeName, before)
	_ = db.Callback().Raw().Before("gorm:raw").Register(callBackBeforeName, before)

	_ = db.Callback().Create().After("gorm:after_create").Register(callBackAfterName, after)
	_ = db.Callback().Query().After("gorm:after_query").Register(callBackAfterName, after)
	_ = db.Callback().Delete().After("gorm:after_delete").Register(callBackAfterName, after)
	_ = db.Callback().Update().After("gorm:after_update").Register(callBackAfterName, after)
	_ = db.Callback().Row().After("gorm:row").Register(callBackAfterName, after)
	_ = db.Callback().Raw().After("gorm:raw").Register(callBackAfterName, after)

	return nil
}

func before(db *gorm.DB) {
	db.InstanceSet(startTime, time.Now())
}

func after(db *gorm.DB) {
	_ts, isExist := db.InstanceGet(startTime)
	if !isExist {
		return
	}
	ts, ok := _ts.(time.Time)
	if !ok {
		return
	}
	fmt.Printf("sql cost time: %v", time.Since(ts).Seconds())
}
复制代码

这里用到了 InstanceGet 和 InstanceSet,本质是在 Statement 中维护了一个 sync.Map 提供链路级别的存储和读取能力。

// InstanceSet store value with key into current db instance's context
func (db *DB) InstanceSet(key string, value interface{}) *DB {
	tx := db.getInstance()
	tx.Statement.Settings.Store(fmt.Sprintf("%p", tx.Statement)+key, value)
	return tx
}

// InstanceGet get value with key from current db instance's context
func (db *DB) InstanceGet(key string) (interface{}, bool) {
	return db.Statement.Settings.Load(fmt.Sprintf("%p", db.Statement) + key)
}
复制代码

参考资料

猜你喜欢

转载自juejin.im/post/7097999127300014093