GORM Playground Link
https://github.com/go-gorm/playground/pull/602
Description
We recently updated our gorm dependency and we had some regression tests fail. During our investigation we discovered that when the there's a gorm tag of default:null the value from the database is not set in the returned resource.
We would expect that anytime a resource is read that it's value should match the values in the database, regardless of any gorm tags.
Comment From: black-06
This is really strange.
Here data is nil so the field is not set. https://github.com/go-gorm/gorm/blob/11fdf46a9fcc393e604ea6df22ce0355b2fe1afb/schema/field.go#L797-L800
Does this need to be fixed? @a631807682
Comment From: a631807682
We do not recommend re-using the model in the finished api, it may have worked in some intermediate version of gorm for some reason, but this was actually an accident, due to a bug caused by an optimization commit. For specific reasons, see the issues associated refer to https://github.com/go-gorm/gorm/pull/6219
Comment From: jimlambrt
I feel like this is a breaking change... we used this successfully in the following versions:
v1.24.5
v1.23.8
1.22.3
v1.21.17-0.20211013130203-9a5ba3760424
v1.21.14
Comment From: a631807682
Need to investigate
Comment From: a631807682
This behavior was last changed in v1.23.0. It is not expected that such behavior occurs after this version. Sorry, this feat is no longer supported as we are unable to support both this feat and embedded.
Comment From: jimlambrt
I respect your decision as the maintainers, but it would seem trivial to support both embedded and this behavior by simply adding a function call before/while reading the resource like:
func clearNullResourceFields(ctx context.Context, db *gorm.DB, i interface{}) error {
stmt := db.Model(i).Statement
if err := stmt.Parse(i); err != nil {
return err
}
v := reflect.ValueOf(i)
for _, f := range stmt.Schema.Fields {
switch {
case f.PrimaryKey:
continue
case !strings.EqualFold(f.DefaultValue, "null"):
continue
default:
_, isZero := f.ValueOf(ctx, v)
if isZero {
continue
}
if err := f.Set(stmt.Context, v, f.DefaultValueInterface); err != nil {
return fmt.Errorf("unable to set value of non-zero field: %w", err)
}
}
}
return nil
}
Comment From: a631807682
I respect your decision as the maintainers, but it would seem trivial to support both embedded and this behavior by simply adding a function call before/while reading the resource like:
func clearNullResourceFields(ctx context.Context, db *gorm.DB, i interface{}) error { stmt := db.Model(i).Statement if err := stmt.Parse(i); err != nil { return err } v := reflect.ValueOf(i) for _, f := range stmt.Schema.Fields { switch { case f.PrimaryKey: continue case !strings.EqualFold(f.DefaultValue, "null"): continue default: _, isZero := f.ValueOf(ctx, v) if isZero { continue } if err := f.Set(stmt.Context, v, f.DefaultValueInterface); err != nil { return fmt.Errorf("unable to set value of non-zero field: %w", err) } } } return nil }
This solves the problem to a certain extent, but doesn't fit well in gorm.
1. Since automatic migration is not required, default:null is not required, and may not match in this case
2. Due to version changes, this function is invalid, re-adding it may be a breaking change for the current version, because I don't know if the current user needs to reset its value (this may bring new permission definitions, because if you disable read permission, gorm should not change its value, and its changes may also be related to hooks).
3. Judging from the current feedback, not many users need this function, which will bring a certain amount of overhead.
Comment From: macmacbr
I think I found a solution for this: If you have a type serializer, you can add the logic proposed above there. (Code may not be correct due to simplification, but the gist is there through the Scan and Value calls).
type MyType struct { Val string }
var myTypeNull := &MyType{}
func (m *MyType) fromString(s string) *MyType {
if s == null { return MyTypeNull; }
return &MyType{Val: s}
}
func (m MyType) Valid() bool {
return true
}
func (es *MyType) Scan(ctx context.Context, field *schema.Field, dst reflect.Value, dbValue interface{}) (err error) {
var v MyType
switch value := dbValue.(type) {
case []byte:
v, err = es.fromString(string(value))
case string:
v, err = es.fromString(value)
case nil:
es = MyTypeNull
default:
err = fmt.Errorf("unsupported data %#v", dbValue)
}
if err != nil {
*es = v
}
return
}
func (es MyType) WhenNullValue(fieldNotNullable bool) interface{} {
if fieldNotNullable {
return MyTypeNull.Val
} //else
return nil
}
func (es MyType) Value(ctx context.Context, field *schema.Field, dst reflect.Value, fieldValue interface{}) (interface{}, error) {
if es == *MyTypeNull {
return es.WhenNullValue(field.NotNull), nil
}
if es.MyType.Valid() {
logger.Warn("Invalid MyType found in the DB")
return es.WhenNullValue(field.NotNull), e
}
return es.Val, nil
}
type SomeTable {
gorm.BaseModel //Id, Created, etc..
instance1 MyType `gorm:"default:null"`
instance2 MyType
}
This seems to work for when using the default:null annotation and for when using the embedded with default:null as well.