Edit 1: removed package gorm; replaced "supports configuring multiple database types" with "supports configuring different database engines"; replaced "database types" with "database engines".
Describe the feature
Support for overriding model field tags at runtime by introducing a new StructTagger interface described below:
// StructTagger is implemented by models that need to override the literal tag
// string in a struct field at runtime.
//
// For models that have implemented the StructTagger, the return value of
// [StructTagger.GormStructTag] always takes precedence, but falls back to the
// literal tag string in the struct field if the return value is empty.
//
// For the example below, the flavored tag `gorm:"type:tinyint unsigned"` will
// be used for the Kind field if the database name is "mysql".
//
// type Book struct {
// ID int64
// Name string `gorm:"type:text"`
// Kind BookKind `gorm:"type:smallint unsigned"`
// }
//
// type BookKind uint8
//
// const (
// BookKindFiction BookKind = iota
// BookKindHistory
// BookKindPhilosophy
// )
//
// var dbFlavoredBookStructTags = map[string]map[string]string{
// "postgres": {},
// "mysql": {"Kind": `gorm:"type:tinyint unsigned"`},
// "sqlite": {},
// }
//
// func (Book) GormStructTag(db *gorm.DB, field string) string {
// tags, ok := dbFlavoredBookStructTags[db.Name()]
// if ok {
// return tags[field]
// }
// return ""
// }
type StructTagger interface {
GormStructTag(db *DB, field string) string
}
Motivation
I believe there is a demand to override model struct tags at runtime. For example, in a service that supports configuring different database engines, developers struggle to write struct tags for GORM that can adapt to all database engines due to differences in column types, database features, and more.
Currently, there is no way here in GORM to write different struct tags for different database engines. Therefore, I'm presenting this proposal.
To ensure non-invasiveness and minimize implementation, I propose adding the StructTagger interface described above. One advantage of this solution is that for current users (i.e. for models that do not implement the StructTagger interface), this solution will not cause any performance hit.
Related Issues
TBD...
Comment From: black-06
Currently, there is no way here in GORM to write different struct tags for different database types. Therefore, I'm presenting this proposal.
You can specify the same table name for different structs and then use them for different databases. For example, https://github.com/go-gorm/gorm/blob/206613868439c5ee7e62e116a46503eddf55a548/tests/serializer_test.go#L17-L53
Comment From: aofei
Hi @black-06, thanks for your reply. That's actually what I'm doing right now, but I don't think that's a good solution. The problem with that approach is that we have to use different structs for different database types, which is quite cumbersome.
All I wanna do is write down my model definitions and never care about them, nor about the database type.
I want something as simple as this:
db.Create(&Book{Name: "foo", Kind: BookKindFiction})
not this:
var book interface{}
switch db.Name() {
case "postgres":
book = &BookPostgres{Name: "foo", Kind: BookKindFiction}
case "mysql":
book = &BookMySQL{Name: "foo", Kind: BookKindFiction}
case "sqlite":
book = &BookSQLite{Name: "foo", Kind: BookKindFiction}
}
db.Create(book)
Comment From: black-06
I don't think that's a good solution. The problem with that approach is that we have to use different structs for different database types, which is quite cumbersome.
And implementing the schema.GormDataTypeInterface interface is also a way. JSONType is an example.
I'm noncommittal about new StructTagger interface.
Comment From: aofei
I just made some slight adjustments to my proposal, mainly by replacing "database types" with "database engines", just in case someone mistakenly thinks that I'm talking about data type differences in databases.
Comment From: aofei
And implementing the
schema.GormDataTypeInterfaceinterface is also a way. JSONType is an example.
This existing design hints that the StructTagger interface should be added to the schema package rather than the gorm package. Thanks for bringing it up!
Comment From: a631807682
Are you referring to the GormDataTypeInterface? https://github.com/go-gorm/gorm/blob/206613868439c5ee7e62e116a46503eddf55a548/migrator/migrator.go#L44 https://github.com/go-gorm/datatypes/blob/master/json_type.go#L68
Comment From: aofei
Hi @a631807682,
As I said in https://github.com/go-gorm/gorm/issues/6437#issuecomment-1615321990. No, I'm not talking about GormDataTypeInterface. I'm talking about a feature that provides a way to override the entire struct tag of a certain field, not just part of it.
Comment From: a631807682
Hi @a631807682,
As I said in #6437 (comment). No, I'm not talking about
GormDataTypeInterface. I'm talking about a feature that provides a way to override the entire struct tag of a certain field, not just part of it.
The only possibility is to add an interface that can dynamically change the schema.Field, and this interface conflicts with all current interfaces. I can't see from the examples that it's necessary, would other attributes be different for different databases besides the type? If different should they be defined as one model? This seems to be the feat of gorm/gen. Gorm uses tags to provide better ease of use, but at the same time it is difficult to do such support (like ent, which completely defines the structure through code, may be more suitable for such a scenario), I prefer to achieve it through code generation.
Comment From: aofei
The only possibility is to add an interface that can dynamically change the schema.Field, and this interface conflicts with all current interfaces.
Before I posted the OP, I only did a brief look at GORM's source code, so I'm not sure if I got it right. In my opinion the implementation of this feature should be simple and non-invasive.
For example, for schema.ParseField, all we need to do is something like:
type Schema struct {
// ...
+ dest interface{}
}
func (schema *Schema) ParseField(fieldStruct reflect.StructField) *Field {
var (
err error
tagSetting = ParseTagSetting(fieldStruct.Tag.Get("gorm"), ";")
)
field := &Field{
Name: fieldStruct.Name,
DBName: tagSetting["COLUMN"],
BindNames: []string{fieldStruct.Name},
FieldType: fieldStruct.Type,
IndirectFieldType: fieldStruct.Type,
StructField: fieldStruct,
Tag: fieldStruct.Tag,
TagSettings: tagSetting,
Schema: schema,
Creatable: true,
Updatable: true,
Readable: true,
PrimaryKey: utils.CheckTruth(tagSetting["PRIMARYKEY"], tagSetting["PRIMARY_KEY"]),
AutoIncrement: utils.CheckTruth(tagSetting["AUTOINCREMENT"]),
HasDefaultValue: utils.CheckTruth(tagSetting["AUTOINCREMENT"]),
NotNull: utils.CheckTruth(tagSetting["NOT NULL"], tagSetting["NOTNULL"]),
Unique: utils.CheckTruth(tagSetting["UNIQUE"]),
Comment: tagSetting["COMMENT"],
AutoIncrementIncrement: 1,
}
+ if tagger, ok := schema.dest.(StructTagger); ok {
+ tag := tagger.GormStructTag(schema, fieldStruct.Name)
+ if tag != "" {
+ field.Tag = tag
+ }
+ }
// ...
}
Although this would require a change to the proposal to accept schema.Schema instead of gorm.DB like this:
type StructTagger interface {
GormStructTag(schema *Schema, field string) string
}
or even this:
type StructTagger interface {
GormStructTag(schema *Schema, field reflect.StructField) string
}
Once again, I don't get the whole picture of GORM's source code yet. So, I apologize if I made any mistakes or unintentionally misrepresented something.
Comment From: aofei
Obviously I was wrong, the schema package isn't aware of the gorm package, due to package import cycle. Which also means that schema.Schema isn't aware of things like database engine names. So my thinking in https://github.com/go-gorm/gorm/issues/6437#issuecomment-1616299111 is incorrect.
We may need to keep my OP's design and add StructTagger to the gorm package:
package gorm
type StructTagger interface {
GormStructTag(db *DB, field string) string
}
and have something like this in the schema package:
package schema
type Schema struct {
...
+ tagger func(field string) string
}
+ func ParseWithTagger(dest interface{}, cacheStore *sync.Map, namer Namer, tagger func(field string) string) (*Schema, error) { ... }
func (schema *Schema) ParseField(fieldStruct reflect.StructField) *Field {
+ if schema.tagger != nil {
+ tag := tagger(fieldStruct.Name)
+ if tag != "" {
+ fieldStruct.Tag = reflect.StructTag(tag)
+ }
+ }
var (
err error
tagSetting = ParseTagSetting(fieldStruct.Tag.Get("gorm"), ";")
)
field := &Field{
Name: fieldStruct.Name,
DBName: tagSetting["COLUMN"],
BindNames: []string{fieldStruct.Name},
FieldType: fieldStruct.Type,
IndirectFieldType: fieldStruct.Type,
StructField: fieldStruct,
Tag: fieldStruct.Tag,
TagSettings: tagSetting,
Schema: schema,
Creatable: true,
Updatable: true,
Readable: true,
PrimaryKey: utils.CheckTruth(tagSetting["PRIMARYKEY"], tagSetting["PRIMARY_KEY"]),
AutoIncrement: utils.CheckTruth(tagSetting["AUTOINCREMENT"]),
HasDefaultValue: utils.CheckTruth(tagSetting["AUTOINCREMENT"]),
NotNull: utils.CheckTruth(tagSetting["NOT NULL"], tagSetting["NOTNULL"]),
Unique: utils.CheckTruth(tagSetting["UNIQUE"]),
Comment: tagSetting["COMMENT"],
AutoIncrementIncrement: 1,
}
...
}
Then we can replace schema.Parse with schema.ParseWithTagger in the gorm package.
Like in here:
https://github.com/go-gorm/gorm/blob/206613868439c5ee7e62e116a46503eddf55a548/scan.go#L203-L205
we can change it to:
if reflectValueType != sch.ModelType && reflectValueType.Kind() == reflect.Struct {
sch, _ = schema.ParseWithTagger(db.Statement.Dest, db.cacheStore, db.NamingStrategy, tagger func(field string) string {
if tagger, ok := db.Statement.Dest.(StructTagger); ok {
return tagger.GormStructTag(db, field)
}
return ""
})
}
This design can solve problems like package import cycles. Although it could probably be polished, it remains a viable option. And most importantly, it doesn't conflict with any existing interfaces.
Comment From: jinzhu
Checkout https://gorm.io/docs/models.html#Fields-Tags