カメニッキ

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

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版)のデバッグ機能

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

schema

mysql> desc events;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| id    | int(10)      | YES  |     | NULL    |       |
| name  | varchar(255) | YES  |     | NULL    |       |
| flag  | tinyint(1)   | YES  |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
3 rows in set (0.00 sec)

問題

type Event struct {
  Id   uint   `json:"id"`
  Name string `json:"name"`
  Flag bool  `json:"flag"`
}

func gormConnect() *gorm.DB {
  db, err := gorm.Open("mysql", "root@tcp(localhost:3306)/sample")

  if err != nil {
    panic(err.Error())
  }
  return db
}

func hoge(c echo.Context) {
        db := gormConnect()

        // 初期データ作成
        eventEx := Event{Id: 1, Name: "taro", Flag: true}
        result := db.Create(eventEx)
        fmt.Println(result)

        // 初期データのID=1を更新する
        eventExBefore := Event{Id: 1}
        db.First(&eventExBefore)

        eventExAfter := new(Event)
        // {Name: "jiro", Flag: false} という値がPUTされてきたとする
        c.Bind(eventExAfter)
        db.Model(&eventExBefore).UpdateColumns(&eventExAfter)
}

Createのタイミングで以下のINSERT文が発行される

INSERT INTO `events` (`id`,`name`,`flag`) VALUES (1,'taro',1);

Updateで以下のSQL発行を期待するが、、、

UPDATE `events` SET `name` = 'jiro', `flag` = 0  WHERE `events`.`id` = 1;

実際には以下のSQLが発行される

UPDATE `events` SET `name` = 'jiro'  WHERE `events`.`id` = 1;

以下のとおり、bool型ゼロ値ではないtrueを指定すると

func main() {
        db := gormConnect()

        // 初期データ作成
        eventEx := Event{Id: 1, Name: "taro", Flag: true}
        result := db.Create(eventEx)
        fmt.Println(result)

        // 初期データのID=1を更新する
        eventExBefore := Event{Id: 1}
        db.First(&eventExBefore)

        eventExAfter := new(Event)
        // {Name: "jiro", Flag: true} という値がPUTされてきたとする
        c.Bind(eventExAfter)
        db.Model(&eventExBefore).UpdateColumns(&eventExAfter)
}

期待どおり動作する

INSERT INTO `events` (`id`,`name`,`flag`) VALUES (1,'taro',0);
UPDATE `events` SET `name` = 'jiro', `flag` = 1  WHERE `events`.`id` = 1;

なんでこうなるのか

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

tapira.hatenablog.com

以下のように map[string]interface{}{} に更新パラメータをバインドさせてUpdateすれば意図した動きになるが、Structに定義されたカラム全てが載ってしまい、思った使い方ができなかった。

event := map[string]interface{}{}
    if err := c.Bind(&params); err != nil {
        return err
    }

期待どおりに動かすには

boolポインタ型を使用する。

--- a/gorm-sample.go
+++ b/gorm-sample.go
@@ -10,7 +10,7 @@ import (
 type Event struct {
        Id   uint   `json:"id"`
        Name string `json:"name"`
-       Flag bool   `json:"flag"`
+       Flag *bool  `json:"flag"`
 }

 func gormConnect() *gorm.DB {
@@ -24,15 +24,17 @@ func gormConnect() *gorm.DB {

 func main() {
        db := gormConnect()
+       ptrue := &[]bool{true}[0]
+       pfalse := &[]bool{false}[0]

        // 初期データ作成
-       eventEx := Event{Id: 1, Name: "taro", Flag: false}
+       eventEx := Event{Id: 1, Name: "taro", Flag: ptrue}
        result := db.Create(eventEx)
        fmt.Println(result)

        // 初期データのID=1を更新する
        eventExBefore := Event{Id: 1}
        db.First(&eventExBefore)

        eventExAfter := new(Event)
        // {Name: "jiro", Flag: false} という値がPUTされてきたとする
        c.Bind(eventExAfter)
        db.Model(&eventExBefore).UpdateColumns(&eventExAfter)
 }

こうするとboolポインタ型のゼロ値はnilとなり、falseと値を指定した場合も意図した動きになる。

        eventExAfter := map[string]interface{}{}
        // {Name: "jiro", Flag: false} という値がPUTされてきたとする
        c.Bind(eventExAfter)
        db.Model(&eventExBefore).UpdateColumns(&eventExAfter)

ちなみに、この挙動は github.com/go-playground/validator を使用した際にも同じだった。 flag: falseと指定していた場合もrequire指定でエラーになってしまう。

uintなどを使用した場合にもゼロ値が 0 であるため、0で更新したい場合などは↑のようにuintポインタ型にしないとだめそう。 他にうまい回避方法がわからなくて、こんなことしたけど実はイケてるやり方があったら知りたい。。。

二泊三日で沖縄本島に行ってきた

浮かれ気分のまままとめる

飛行機

Peach Aviation を利用

  • 往路: 福岡 - 那覇 10:30発
  • 復路: 那覇 - 福岡 17:55発

大人2名 19,720円

f:id:tapira:20170709011502j:plain


ピーチは那覇空港LCC専用ターミナル に発着する。 デカイ倉庫みたいなところで、最小限のお土産屋と飲食店だけで構成されている。 注意点は以下 - 無料シャトルバス または 通行許可されたレンタカー会社のバス だけが出入り可能で、歩いてやタクシーで外に出れない - 那覇空港本体と比べて色々ショボい

ネットの不評を見てピーチは失敗したかな?とおもっていたけど以下の 通行許可されたレンタカー を予約しておけば全く問題なかった。 - OTSレンタカー - オリックスレンタカー - トヨタレンタリース - ニッポンレンタカー - ルフト・トラベルレンタカー

レンタカー

今回使用したのは ルフト・トラベルレンタカー 無料でLCCターミナル - 営業所間の送迎をしてくれる。所要時間は15分くらい?

料金がとにかくやすかった。 たびらい というサイト経由で予約したところ、 二泊三日 免責補償 ノンオペレーションチャージ負担無し 返却時ガソリン満タン不要 ハイブリット車10,500円

なんか裏があるのでは!?と思ったけど何もなかった。

ホテル

オクマ プライベートビーチ & リゾート(元 JALプライベートリゾート オクマ) の一番安い パームコテージ プールとかフロントがきれいになってた。部屋は古いまま。 大浴場あって○ ビーチはきれいに掃除されてて○ プールもきれいで○ 室内の4G回線電波が悪くて困った…。ホテルの無料WiFiもない。どうにかならんかなと思った 朝食はビュッフェ形式で、そこそこ良い内容だった。常に係がつきっきりのジューサーが印象的だった。すぐ詰まるらしい 五万円くらいだった。 f:id:tapira:20170709011819j:plain

いったところ

  • 新山食堂
    • 沖縄そば屋。今まで食べた中で一番好みだった。 f:id:tapira:20170709011858j:plain
  • BeansStore
    • コーヒー屋。アイスコーヒーがうまかった。焙煎されてない生?のやつ。天神のステレオカフェ(桜十字病院の裏にある方)で出してるやつがにてた
  • ネオパークオキナワ
    • いわゆる動物園。看板で オオハシ推ししてたのに園内にいなかった。アルダブラゾウガメの甲羅の上に寝て写真撮った
    • ねおねおねねおねおぱーくっていう曲が無限ループで流れてて頭がおかしくなるかとおもった。それなりに楽しめて良い施設だった
  • 安田くいなふれあい公園
    • 前に来た時道路うろうろしたけどみれなかったから、生きたヤンバルクイナ きょんきょんが展示されてるここにきた。初めてみた
    • 帰り道野生のヤンバルクイナおった・・・。初めてみた f:id:tapira:20170709011531j:plain
  • パーラーMOMO
    • ホテルの傍にあったかなり古い飲食店
    • ロコモコ丼(500円)とチーズバーガー(400円)食べたけど、期待を遥かに超えて美味しかった
  • 米軍保養所の右手にあるビーチ
    • 毎回誰もいなくてきれいなビーチ最高- f:id:tapira:20170709011804j:plain f:id:tapira:20170709012330j:plain
  • やまかわ軒
    • ホテルのそばにあった定食屋
    • レバニラ定食(720円)とステーキ定食(1500円)食べた。期待を遥かに超えて美味しかった。ホテルのディナー高いわりに内容普通だったからこっちのが良いと思った
  • フルーツランド
    • 果樹園 + 鳥園
    • 大量のゴシキセイガイインコ放し飼い + チョウゲンボウやコノハズク、保護された鳥がいてめっちゃ楽しかった。 f:id:tapira:20170709011549j:plain
  • 道の駅 大宜味
    • 普通の道の駅
    • ゴールデンバレルという種類のパイナップル食べた。過去最高にうまいパイナップルだった。(どうやら高級品)
  • やんばる野生生物保護センター
    • 資料館
    • 右手にきれいな川が流れてて、川遊びできる。穴場なのでは!? f:id:tapira:20170709012238j:plain
  • 道の駅 許田
    • 毎回ここで60円の魚てんぷら買って食べてる
  • 古宇利島
    • 有名なので割愛
  • 花人逢
    • 途中で引き返した。有名なので割愛
  • メキシコ
    • タコス屋。うまかった〜〜〜〜〜〜
  • 安田共同店
    • 「あだ」らしい
    • 100円のコーヒー飲んだ
    • 近くのビーチで釣りキャンプしてる3人組いて楽しそうだった
  • うるま?の海中道路
    • 海の中に潜って走れるのかと勘違いしてたら、目線の高さに海が見える道路だった f:id:tapira:20170709012307j:plain

まとめ

鳥観察に比重を置いた、のんびり系の旅行でした。 LCCターミナル、個人的には全然苦にならないので、こんだけ安ければもっと気軽にきてもいいなーと思った。 前回食事が外れまくって辛かったけど、今回食べたもの全部美味しかった〜 (とくにまとまらなかった)

水上悟志 「スピリットサークル」がおもしろい

f:id:tapira:20170630233739p:plain

輪廻転生もの 全6巻

何も知らない中学二年生の桶屋風太(おけや ふうた)と、ある日やってきた転校生 石神鉱子(いしがみ こうこ)の前世から続く因縁を、スピリットサークルという道具を使用して前世の記憶を再体験し、解明していくお話(?)

漫画のレビューとか普段全然しないのだけど、久しぶりにこの漫画めっちゃおもしろい!ってなったので紹介…。

短編集のような構成になっていて、様々な時代で様々な役柄の話があって飽きない

短いし、最初の1巻だけとりあえず読めばあうあわない分かると思うからとりあえず読んでみてほしい

PHPカンファレンス福岡2017のスポンサーセッションで「mrubyで作る海外IPフィルター」という話をした

スライド

speakerdeck.com

発表の振り返り

  • 思ったよりも落ち着いて喋ることができた
  • 正面を向いて話すよう意識できたのはよかった
  • 本番よりも練習の方が話しづらい。本番はむしろやりやすい(重要)
  • 練習大事…本当に練習大事

スライド作成の振り返り・今後意識したいこと

  • プロジェクターの画面比率は早めに確認しよう
    • 16:9で作ってたのを大至急4:3にして辛い思い
  • カンファレンスの雰囲気・規模・どういった立場で喋るのか意識しよう
    • スポンサーセッションは固めの文章、明るくやるならトーク
    • 小規模なLTなどであればフレンドリーな文面も可 など
  • どういった層を相手に話すか意識しよう
    • 技術レベル
    • 使う言葉が一般的なのか、内輪向けの話なのか、単語の定義を意識
  • どういうタイトル何が言いたいか をまず決めよう
  • スライドの流れ・ストーリーを意識しよう
    • 守らないと結局何が言いたいの?なスライドになる
  • スライド作成前にアウトラインを固めよう
    • トーリー作りがスライドだとやり辛い、めちゃくちゃなものができやすい
    • スライドへでのレビューは面倒(レビュワーへの負担)
  • 箇条書きは一行で。改行避ける
  • 文章で伝えづらいところだけ図にしよう
    • わかりやすくするための図がわかりにくい問題
  • テーマを統一しよう
    • いろんな画像混ぜない
  • 文字はでかいほどよい
  • 自分自身で読み直す時必ず声に出したほうが問題に気づきやすい
  • プロジェクタにうつしてレビューするとただ眺めるだけよりも多くの問題見えてくる

(※あくまでも僕個人の思い)

PHPカンファレンス運営スタッフの皆様、スライドづくりのご協力いただいたペパボの皆様、当日セッションを見に来てくれた方、ありがとうございました!!!

.
.
.
.
.
.
.
.
.
.
.
.
.
(おまけ)keynote発表者ディスプレイをカスタマイズ の残り時間設定が罠だった…w
15:00 で15分じゃなくて15時間… どんなプレゼンだw f:id:tapira:20170610175007p:plain