satoshi.inoue's blog

備忘録を兼ねているので、薄い内容の投稿もあります。

gormのupdateのハマりどころ

gormでupdateするときのハマりどころをまとめました。

gormのUpdatesについて

// 下記のレコードが登録済み
//   id | name | activated
// -----+------+----------
//    1 |  Bob |      true

id = 1のactivatedをfalseに更新する方法をいくつか挙げます。

structを渡す場合

beforeUser := User{
    ID: 1,
}

// activatedをfalseに更新する
db.Model(&beforeUser).Updates(User{Activated: false})

afterUser := User{}
// 1レコード目を取得する
db.First(&afterUser)

// 更新できたか確認
fmt.Printf("ID: %d, Activated: %v\n", afterUser.ID, afterUser.Activated)

// ID: 1, Activated: true

更新できていませんでした。

mapを渡す場合

beforeUser := User{
    ID: 1,
}

// activatedをfalseに更新する
db.Model(&user).Updates(map[string]interface{}{"activated": false})

afterUser := User{}
// 1レコード目を取得する
db.First(&afterUser)

// 更新できたか確認
fmt.Printf("ID: %d, Activated: %v\n", afterUser.ID, afterUser.Activated)

// ID: 1, Activated: false

正しく更新できました。

なぜこのようなことが起こるのか

gorm/scope.go

func (scope *Scope) Fields() []*Field {
    if scope.fields == nil {
        var (
            fields             []*Field
            indirectScopeValue = scope.IndirectValue()
            isStruct           = indirectScopeValue.Kind() == reflect.Struct
        )

        for _, structField := range scope.GetModelStruct().StructFields {
            if isStruct {
                fieldValue := indirectScopeValue
                for _, name := range structField.Names {
                    fieldValue = reflect.Indirect(fieldValue).FieldByName(name)
                }
                fields = append(fields, &Field{StructField: structField, Field: fieldValue, IsBlank: isBlank(fieldValue)})
            } else {
                fields = append(fields, &Field{StructField: structField, IsBlank: true})
            }
        }
        scope.fields = &fields
    }

    return *scope.fields
}

func isBlank(value reflect.Value) bool {
    return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface())
}

func convertInterfaceToMap(values interface{}) map[string]interface{} {
    var attrs = map[string]interface{}{}

    switch value := values.(type) {
    case map[string]interface{}:
        return value
    case []interface{}:
        for _, v := range value {
            for key, value := range convertInterfaceToMap(v) {
                attrs[key] = value
            }
        }
    case interface{}:
        reflectValue := reflect.ValueOf(values)

        switch reflectValue.Kind() {
        case reflect.Map:
            for _, key := range reflectValue.MapKeys() {
                attrs[ToDBName(key.Interface().(string))] = reflectValue.MapIndex(key).Interface()
            }
        default:
            for _, field := range (&Scope{Value: values}).Fields() {
                if !field.IsBlank {
                    attrs[field.DBName] = field.Field.Interface()
                }
            }
        }
    }
    return attrs
}

大事なのは上記の1メソッドと2関数。
Fields()はstructの場合、各フィールドに値が入っているかをisBlankという関数を用いて判定している。
isBlankは各型ごとのゼロ値かどうかを判定しているが、falseに更新したい場合はisBlankがtrueになってしまう。
convertInterfaceToMapで最終的に更新するフィールドと値をセットしているが、structの場合は case interface{}: に該当し、Mapではない為 default: が処理される。
ここで IsBlank = true だと更新対象外となってします。

一方 map[string]interface{} で更新する場合は、isBlankは関係なく case map[string]interface{}: が処理される為、もれなく更新されます。

まとめ

当たり前のように更新されると思いきや、実装方法によっては更新されない場合があるので、どういう場合に更新されるのか、ライブラリの実装をちゃんと読んで理解することは大切ですね。