カメニッキ

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

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ポインタ型にしないとだめそう。 他にうまい回避方法がわからなくて、こんなことしたけど実はイケてるやり方があったら知りたい。。。