前言
使用Gorm
构造模型时,通过实现Tabler interface
的TableName()
func可以指定table的名称。那么
TableName()
何时调用?- 操作时每次都会调用
TableName()
吗? TableName()
支持动态表名吗?- 怎样实现动态表名?
更多内容分享,欢迎关注公众号: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()
来实现动态表名。