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.