Gorm TableName相关

前言

使用Gorm构造模型时,通过实现Tabler interfaceTableName() func可以指定table的名称。那么

  1. TableName()何时调用?
  2. 操作时每次都会调用TableName()吗?
  3. TableName()支持动态表名吗?
  4. 怎样实现动态表名?

更多内容分享,欢迎关注公众号:Go开发笔记

TableName

Gorm增删改查等操作最终交由Excute执行。

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

Excute

func (p *processor) Execute(db *DB) {
	curTime := time.Now()
	stmt := db.Statement

	if stmt.Model == nil {// 未指定model时,默认使用dest作为model
		stmt.Model = stmt.Dest
	} else if stmt.Dest == nil {// 未指定dest时,默认指定model为dest
		stmt.Dest = stmt.Model
	}

	if stmt.Model != nil {
        // 解析Model
		if err := stmt.Parse(stmt.Model); err != nil && (!errors.Is(err, schema.ErrUnsupportedDataType) || (stmt.Table == "" && stmt.SQL.Len() == 0)) {
			if errors.Is(err, schema.ErrUnsupportedDataType) && stmt.Table == "" {
				db.AddError(fmt.Errorf("%w: Table not set, please set it like: db.Model(&user) or db.Table(\"users\")", err))
			} else {
				db.AddError(err)
			}
		}
	}

	if stmt.Dest != nil {
		stmt.ReflectValue = reflect.ValueOf(stmt.Dest)
		for stmt.ReflectValue.Kind() == reflect.Ptr {
			stmt.ReflectValue = stmt.ReflectValue.Elem()
		}
		if !stmt.ReflectValue.IsValid() {
			db.AddError(fmt.Errorf("invalid value"))
		}
	}

	for _, f := range p.fns {
		f(db) // 执行具体的增删改查(包含增删改查前后的操作)
	}

	db.Logger.Trace(stmt.Context, curTime, func() (string, int64) {
		return db.Dialector.Explain(stmt.SQL.String(), stmt.Vars...), db.RowsAffected
	}, db.Error)

	if !stmt.DB.DryRun {
		stmt.SQL.Reset()
		stmt.Vars = nil
	}
}

Excute时处理步骤如下:

  • 未指定Model时,默认使用Dest(接收数据者)作为model
  • 未指定Dest,则使用Model
  • Model不为nil时,开始解析Model
  • 校验stmt.Dest
  • 执行具体的增删改查(包含增删改查前后的操作)
  • logger跟踪日志

Parse

Model具体解析过程如下:

func (stmt *Statement) Parse(value interface{}) (err error) {
    // 只有stmt.Table为空时,才会使用Model解析获得的TableName
	if stmt.Schema, err = schema.Parse(value, stmt.DB.cacheStore, stmt.DB.NamingStrategy); err == nil && stmt.Table == "" {
		if tables := strings.Split(stmt.Schema.Table, "."); len(tables) == 2 {
			stmt.TableExpr = &clause.Expr{SQL: stmt.Quote(stmt.Schema.Table)}
			stmt.Table = tables[1]
			return
		}

		stmt.Table = stmt.Schema.Table
	}
	return err
}

// get data type from dialector
func Parse(dest interface{}, cacheStore *sync.Map, namer Namer) (*Schema, error) {
	if dest == nil {
		return nil, fmt.Errorf("%w: %+v", ErrUnsupportedDataType, dest)
	}

	modelType := reflect.ValueOf(dest).Type()
	for modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array || modelType.Kind() == reflect.Ptr {
		modelType = modelType.Elem()
	}

	if modelType.Kind() != reflect.Struct {
		if modelType.PkgPath() == "" {
			return nil, fmt.Errorf("%w: %+v", ErrUnsupportedDataType, dest)
		}
		return nil, fmt.Errorf("%w: %v.%v", ErrUnsupportedDataType, modelType.PkgPath(), modelType.Name())
	}

	if v, ok := cacheStore.Load(modelType); ok {// 查找缓存,存在即复用
		s := v.(*Schema)
		<-s.initialized
		return s, nil
	}

	modelValue := reflect.New(modelType)
	tableName := namer.TableName(modelType.Name()) // 默认根据model名称获取TableName
	if tabler, ok := modelValue.Interface().(Tabler); ok {
		tableName = tabler.TableName() // 实现Tabler,获取指定TableName
	}
	if en, ok := namer.(embeddedNamer); ok {
		tableName = en.Table
	}

	schema := &Schema{
		Name:           modelType.Name(),
		ModelType:      modelType,
		Table:          tableName, // table
		FieldsByName:   map[string]*Field{},
		FieldsByDBName: map[string]*Field{},
		Relationships:  Relationships{Relations: map[string]*Relationship{}},
		cacheStore:     cacheStore,
		namer:          namer,
		initialized:    make(chan struct{}),
	}
    // When the schema initialization is completed, the channel will be closed
	defer close(schema.initialized)
	// 没有则存储,此处可用以处理其他goroutine完成初始化的问题
	if v, loaded := cacheStore.LoadOrStore(modelType, schema); loaded {
		s := v.(*Schema)
		// Wait for the initialization of other goroutines to complete
		<-s.initialized
		return s, s.err
	}
	...
	return schema, schema.err
}

解析主要步骤如下:

  • 查找缓存,如有则直接返回缓存中的数据
  • 若实现了Tabler接口(TableName func),则tableName=TableName()
  • 若内含embeddedNamer,则tableName=Table
  • 构造Schema,缓存中没有则存储
  • 解析field

Model初次解析后会存入缓存,后续使用的均是缓存中的数据(即TableName()只调用了一次),当stmt.Table==''时,会使用解析获得的TableName(),所以如果不提前指定stmt.Table是无法通过TableName()来实现动态表名的。此处回答了问题1、2、3.

stmt.Table何时使用

Orm的最终执行的还是拼凑后的SQL语句上,以Create为例,其对应的是INSERT INTO的语句。

func Create(config *Config) func(db *gorm.DB) {
	if config.WithReturning {
		return CreateWithReturning
	} else {
		return func(db *gorm.DB) {
			if db.Error == nil {
				if db.Statement.Schema != nil && !db.Statement.Unscoped {
					for _, c := range db.Statement.Schema.CreateClauses {
						db.Statement.AddClause(c)
					}
				}

				if db.Statement.SQL.String() == "" {// 没指定SQL时,根据语法生成语句
					db.Statement.SQL.Grow(180)
					db.Statement.AddClauseIfNotExists(clause.Insert{})
					db.Statement.AddClause(ConvertToCreateValues(db.Statement))

					db.Statement.Build("INSERT", "VALUES", "ON CONFLICT") // 构造SQL语句
				}

				if !db.DryRun && db.Error == nil {
					result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...) // 执行SQL语句

                    ...
				}
			}
		}
	}
}

// Insert的build处理过程
func (insert Insert) Build(builder Builder) {
	if insert.Modifier != "" {
		builder.WriteString(insert.Modifier)
		builder.WriteByte(' ')
	}

	builder.WriteString("INTO ")
	if insert.Table.Name == "" {
		builder.WriteQuoted(currentTable)
	} else {
		builder.WriteQuoted(insert.Table)
	}
}

const (
	CurrentTable string = "@@@ct@@@" // current table
)

var (
	currentTable  = Table{Name: CurrentTable}
)

// WriteQuoted write quoted value
func (stmt *Statement) WriteQuoted(value interface{}) {
	stmt.QuoteTo(&stmt.SQL, value)
}

// QuoteTo write quoted value to writer
func (stmt *Statement) QuoteTo(writer clause.Writer, field interface{}) {
	switch v := field.(type) {
	case clause.Table:
		if v.Name == clause.CurrentTable {
			if stmt.TableExpr != nil {
				stmt.TableExpr.Build(stmt)
			} else {
				stmt.DB.Dialector.QuoteTo(writer, stmt.Table)// 具体使用的stmt.Table
			}
		} else if v.Raw {
			writer.WriteString(v.Name)
		} else {
			stmt.DB.Dialector.QuoteTo(writer, v.Name)
		}

		if v.Alias != "" {
			writer.WriteByte(' ')
			stmt.DB.Dialector.QuoteTo(writer, v.Alias)
		}
    ...
	}
}

如何使用动态表名

答案是在执行增删改查操作前,使用Table()指定动态的表名。

// Table specify the table you would like to run db operations
func (db *DB) Table(name string, args ...interface{}) (tx *DB) {
	tx = db.getInstance()
	if strings.Contains(name, " ") || strings.Contains(name, "`") || len(args) > 0 {
		tx.Statement.TableExpr = &clause.Expr{SQL: name, Vars: args}
		if results := tableRegexp.FindStringSubmatch(name); len(results) == 2 {
			tx.Statement.Table = results[1]
			return
		}
	} else if tables := strings.Split(name, "."); len(tables) == 2 {
		tx.Statement.TableExpr = &clause.Expr{SQL: tx.Statement.Quote(name)}
		tx.Statement.Table = tables[1]
		return
	}

	tx.Statement.Table = name
	return
}

指定Table name后,tx.Statement.Table不为空,Model解析后不会再设置Table,后续的操作一直使用的都是指定的Table

**需要注意的是:**若要gorm的默认是clone原DB的Statement后再执行操作的,因此所有涉及到动态表名的操作均需使用Table()后的DB,否则使用的仍是初次解析的TableName()

如下:

// 正确使用示例
wdb := db.RW.Table(model.TableName()) // 指定table,然后执行后续操作
wdb.Find() // query
wdb.Create() // insert

// 错误使用示例
db.Table(model.TableName()).Find() //指定table后随即query
db.Create() // insert,此时并未使用指定的table,仍是初次解析model的TableName()

总结

由于TableName()仅在初次解析后即缓存,后续不再解析,因此无法通过TableName()来实现动态表名,但可以通过Table()来实现动态表名。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值