カメニッキ

カメとインコと釣りの人です

gormのUpdateColumnsでモデル内のboolゼロ値(false)を持ったカラムが更新されなかった の続き

前回の記事(http://tapira.hatenablog.com/entry/2017/08/09/173718)で以下の通りよくわからんくて飛ばしてたところを、gormのコードを追ってみた。

func (s DB) UpdateColumns(values interface{}) DB { ここで呼ばれたタイミングではvaluesに値が渡ってきてるけど、その後instanceにセットしてcallback呼んだタイミングで消えてるので、 Setするどっかのロジックでゼロ値だとスキップするような動きがあると思うけどどこかわからんかった・・・


  • UpdateColumns の呼び出し
db.Model(&eventExBefore).UpdateColumns(&eventExAfter)
  • gorm/callback_update.goのinit関数にて、assignUpdatingAttributesCallbackがコールバック関数に登録されます
// Define callbacks for updating
func init() {
  DefaultCallback.Update().Register("gorm:assign_updating_attributes", assignUpdatingAttributesCallback)
  • その後callCallbacksの呼び出しによって、登録されたコールバック関数が実行されます
func (s *DB) UpdateColumns(values interface{}) *DB {
  return s.clone().NewScope(s.Value).
    Set("gorm:update_column", true).
    Set("gorm:save_associations", false).
    InstanceSet("gorm:update_interface", values).
    callCallbacks(s.parent.callbacks.updates).db
}
  • assignUpdatingAttributesCallbackの中身
// assignUpdatingAttributesCallback assign updating attributes to model
func assignUpdatingAttributesCallback(scope *Scope) {
  if attrs, ok := scope.InstanceGet("gorm:update_interface"); ok {
    if updateMaps, hasUpdate := scope.updatedAttrsWithValues(attrs); hasUpdate {
      scope.InstanceSet("gorm:update_attrs", updateMaps)
    } else {
      scope.SkipLeft()
    }
  }
}

以下の処理で UpdateColumns(&eventExAfter) で渡された値をupdate_attrsにセットしている

    if updateMaps, hasUpdate := scope.updatedAttrsWithValues(attrs); hasUpdate {
      scope.InstanceSet("gorm:update_attrs", updateMaps)
  • updatedAttrsWithValuesの中を追いかけてみると、以下
func (scope *Scope) updatedAttrsWithValues(value interface{}) (results map[string]interface{}, hasUpdate bool) {
  if scope.IndirectValue().Kind() != reflect.Struct {
    return convertInterfaceToMap(value, false), true
  }

  results = map[string]interface{}{}

  for key, value := range convertInterfaceToMap(value, true) {
    if field, ok := scope.FieldByName(key); ok && scope.changeableField(field) {

ゼロ値の場合convertInterfaceToMapにて該当カラムが更新対象列から消える - convertInterfaceToMapは以下

func convertInterfaceToMap(values interface{}, withIgnoredField bool) 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, withIgnoredField) {
        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 && (withIgnoredField || !field.IsIgnored) {
          attrs[field.DBName] = field.Field.Interface()
        }
      }
    }
  }
  return attrs
}
  • 今回はinterface{}で値を受けており、ゼロ値が渡されたカラムは以下の処理でfalseと判定され、更新対象リストにアペンドされていないっぽい
        if !field.IsBlank && (withIgnoredField || !field.IsIgnored) {
          attrs[field.DBName] = field.Field.Interface()
        }
  • 確認してみるとゼロ値のカラムについてはIsBlank=trueとなっていた。IsBlankのセットは以下のコードでやっているぽい
func (field *Field) Set(value interface{}) (err error) {
・・・
field.IsBlank = isBlank(field.Field)
  • isBlankの実装を見てみると
func isBlank(value reflect.Value) bool {
  switch value.Kind() {
  case reflect.String:
    return value.Len() == 0
  case reflect.Bool:
    return !value.Bool()
  case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
    return value.Int() == 0
  case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
    return value.Uint() == 0
  case reflect.Float32, reflect.Float64:
    return value.Float() == 0
  case reflect.Interface, reflect.Ptr:
    return value.IsNil()
  }

  return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface())
}

!value.Bool() false渡したらisBlank=true …

bool型がtrueかfalseしかもてない以上、こうなるのはやむないのか。 ここにたどり着くのにめちゃくちゃ時間かかってしまった。 もっと素早くコールドリーディングできるようになりたい

今回お世話になったツール

Visual Studio Code(Mac版)のデバッグ機能