gormと仲良くなりたい(1) - gorm の Where/Update で struct を使いたい

# 概要
GoでORMを使用する時の定番である [gorm](http://gorm.io/) のお話です。

`SQL Builder` や  `Auto Migrations` 、`structへのauto mapping` 等
ORM に必要な機能が一通り揃っている便利なやつです。
が、こいつを使う際には幾つか注意が必要です。

今回はそのうちの一つ
**gorm で Where/Update の際にstructを使用する**
事を目的として検証していきます。

**Gormを使用したDelete処理は別途特別に注意が必要です
[GormのDELETEで誤爆しないために](https://qiita.com/tomtwinkle/items/48e2fa94ff0624b717b7)
を参照してください。**

# 検証 - 失敗パターン
gorm で struct を使用して検索条件を指定したり、レコードの更新を行おうとすると 正常に動作しない場合があります。

どういうことなのか動きを見ていきましょう。
以下はgormを使って

* Table 作成 (AutoMigrate)
* データ投入 (Delete/Create)
* レコード取得 (Find)

をするだけの単純なコードです。

```go
package main

import (
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"github.com/jinzhu/gorm"
)

type Sample struct {
	Num  int64
	Bool bool
	Str  string
}

func main() {
	db, err := gorm.Open("mysql", "test:test@/sample")
	if err != nil {
		panic("failed to connect database")
	}
	defer db.Close()

	// Migrate the schema
	if err := db.AutoMigrate(&Sample{}).Error; err != nil {
		panic(err.Error())
	}

	// Delete
	db.Delete(&Sample{})

	// Create
	db.Create(&Sample{Num: 0, Bool: false, Str: ""})
	db.Create(&Sample{Num: 1, Bool: true, Str: "sample"})

	var result []Sample

	// 期待する発行SQL: Select * From samples Where Num = 0
	db.Where(&Sample{Num: 0}).Find(&result)
	fmt.Printf("Where Num=0 :\t %+v\n", result)

	// 期待する発行SQL: Select * From samples Where Bool = 0
	db.Where(&Sample{Bool: false}).Find(&result)
	fmt.Printf("Where Bool=0 :\t %+v\n", result)

	// 期待する発行SQL: Select * From samples Where Str = ""
	db.Where(&Sample{Str: ""}).Find(&result)
	fmt.Printf("Where Str=\"\" :\t %+v\n", result)
}
```

データ投入後テーブルには以下のレコードが入っています。

| Num (int) | Bool (int) | Str(varchar) |
|:---------:|:----------:|:------------:|
| 0 | 0 |  ""        |
| 1 | 1 |  "sample"  |

発行される(想定の)SQLは以下の通り

* `Select * From samples Where Num = 0`
* `Select * From samples Where Bool = 0`
* `Select * From samples Where Str = ""`

実行の結果 `{Num:0 Bool:false Str:}` 1レコードだけ取得されることが期待されますが
実行してみると

```
Where Num=0 :	 [{Num:0 Bool:false Str:} {Num:1 Bool:true Str:sample}]
Where Bool=0 :	 [{Num:0 Bool:false Str:} {Num:1 Bool:true Str:sample}]
Where Str="" :	 [{Num:0 Bool:false Str:} {Num:1 Bool:true Str:sample}]
```

全レコード取れてしまいます。

これは `int` , `bool` , `string` の初期値が struct に設定されている場合
未定義(ゼロ値)なのか、検索条件にゼロ値の値を設定しているのか判断出来ないためです。
実際のロジックはgormのコードの[ここらへん](https://github.com/jinzhu/gorm/blob/master/utils.go#L128)を参照。

Updateについても同様です。
例えば、`UPDATE samples SET Bool = 0` を発行する想定で
[Gorm Update](http://gorm.io/ja_JP/docs/update.html) の記載に従って

```Go
	updateSample := Sample{Bool:false}
	db.Model(&Sample{}).Updates(&updateSample)
```

のように書いても実際には更新されません。


これは gorm が悪いというよりは Null値 が存在しない Go の仕様上仕方のない話です。
[公式のDocにもstruct使うときの注意点として以下のように記述があります](http://gorm.io/docs/query.html#Struct-amp-Map)。

> **NOTE** When query with struct, GORM will only query with those fields has non-zero value, that means if your field’s value is 0, '', false or other zero values, it won’t be used to build query conditions

# 解決策1: 条件に `struct` を使用しない

これの解決策として一番単純なものはWhere/Updateで struct を使わない事です。

```Go
	// 期待する発行SQL: Select * From samples Where Num = 0
	//db.Where(&Sample{Num: 0}).Find(&result)
	db.Where("Num = ?", 0).Find(&result)
	fmt.Printf("Where Num=0 :\t %+v\n", result)

	// 期待する発行SQL: Select * From samples Where Bool = 0
	//db.Where(&Sample{Bool: false}).Find(&result)
	db.Where("Bool = ?", false).Find(&result)
	fmt.Printf("Where Bool=0 :\t %+v\n", result)

	// 期待する発行SQL: Select * From samples Where Str = ""
	//db.Where(&Sample{Str: ""}).Find(&result)
	db.Where("Str = ?", "").Find(&result)
	fmt.Printf("Where Str=\"\" :\t %+v\n", result)
```

```
Where Num=0 :	 [{Num:0 Bool:false Str:}]
Where Bool=0 :	 [{Num:0 Bool:false Str:}]
Where Str="" :	 [{Num:0 Bool:false Str:}]
```

Updateの場合はこんな感じ

```Go
	db.Model(Sample{}).Update("Bool", false)
	db.Model(Sample{}).Updates(map[string]interface{}{"Num": 0, "Bool": false, "Str": ""})
```

解決っちゃ解決なのですが
見ての通り、ORMなのにプリペアードクエリ書かないといけないのが  とても  つらい


# 解決策2: `struct` の値を `pointer` で定義する
今回の問題は要するに struct 内の値がゼロ値か未定義か判断出来ない事が根本原因です。
無理やり感がありますが [struct の値を pointer にしてしまえば nil か否かで判断出来ます](https://github.com/jinzhu/gorm/blob/master/utils.go#L140)。


```Go
package main

import (
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"github.com/jinzhu/gorm"
	"reflect"
)

type Sample struct {
	Num  *int64
	Bool *bool
	Str  *string
}

func (s *Sample) Set(num int64, bool bool, str string) {
	s.Num = &num
	s.Bool = &bool
	s.Str = &str
}

// struct の値を文字列で取得する
func (s Sample) ToString() string {
	var str string
	v := reflect.Indirect(reflect.ValueOf(s))
	t := v.Type()
	str = "{"
	for i := 0; i < t.NumField(); i++ {
		str = str + fmt.Sprintf("%v=%v ", t.Field(i).Name, v.Field(i).Elem().Interface())
	}
	str = str + "}"
	return str
}

func main() {
	db, err := gorm.Open("mysql", "test:test@/sample")
	if err != nil {
		panic("failed to connect database")
	}
	defer db.Close()

	// Migrate the schema
	if err := db.AutoMigrate(&Sample{}).Error; err != nil {
		panic(err.Error())
	}

	// Delete
	db.Delete(&Sample{})

	// Create
	var sample Sample
	sample.Set(int64(0), false, "")
	db.Create(&sample)
	sample.Set(int64(1), true, "sample")
	db.Create(&sample)

	var result []Sample

	// 期待する発行SQL: Select * From samples Where Num = 0
	queryNum := int64(0)
	db.Where(&Sample{Num: &queryNum}).Find(&result)
	for _, r := range result {
		fmt.Printf("Where Num = 0:\t %s\n", r.ToString())
	}

	// 期待する発行SQL: Select * From samples Where Bool = 0
	queryBool := false
	db.Where(&Sample{Bool: &queryBool}).Find(&result)
	for _, r := range result {
		fmt.Printf("Where Bool = 0:\t %s\n", r.ToString())
	}

	// 期待する発行SQL: Select * From samples Where Str = ""
	queryStr := ""
	db.Where(&Sample{Str: &queryStr}).Find(&result)
	for _, r := range result {
		fmt.Printf("Where Str = \"\":\t %s\n", r.ToString())
	}
}
```

```
Where Num = 0:	 {Num=0 Bool=false Str= }
Where Bool = 0:	 {Num=0 Bool=false Str= }
Where Str = "":	 {Num=0 Bool=false Str= }
```

書いてみましたがぶっちゃけpointerだと値の操作がめんどくさいです。
でも、きちんと想定通りの動作はします。

この実装で行くなら `ToString()` や `Getter/Setter` をきちんと実装したり
Helper書かないとやってられないですね。
解決策2で上手い実装思いついたらまた更新します。

# 解決策3: `sql.Null` typeを使用する

[Go の database packageで定義されている](https://golang.org/pkg/database/sql/#NullBool) `sql.NullBool` `sql.NullString` `sql.NullInt64` を使う方法です。

`sql.Null` type はNullかゼロ値か判断する為にValidを定義したstructです。

```Go
type NullInt64 struct {
    Int64 int64
    Valid bool // Valid is true if Int64 is not NULL
}
```

実際の実装は以下の通り。
struct の値を `sql.Null` type に置き換え
その値を使用する際に `Valid=true` で使用するようにします。

```Go
package main

import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"github.com/jinzhu/gorm"
)

type Sample struct {
	Num  sql.NullInt64
	Bool sql.NullBool
	Str  sql.NullString
}

func (s *Sample) Set(num int64, bool bool, str string) {
	s.Num = sql.NullInt64{Int64: num, Valid: true}
	s.Bool = sql.NullBool{Bool: bool, Valid: true}
	s.Str = sql.NullString{String: str, Valid: true}
}

func main() {
	db, err := gorm.Open("mysql", "test:test@/sample")
	if err != nil {
		panic("failed to connect database")
	}
	defer db.Close()

	// Migrate the schema
	if err := db.AutoMigrate(&Sample{}).Error; err != nil {
		panic(err.Error())
	}

	// Delete
	db.Delete(&Sample{})

	// Create
	var sample Sample
	sample.Set(0, false, "")
	db.Create(sample)
	sample.Set(1, true, "sample")
	db.Create(sample)

	var result []Sample

	// 期待する発行SQL: Select * From samples Where Num = 0
	db.Where(&Sample{Num: sql.NullInt64{Int64: 0, Valid: true}}).Find(&result)
	fmt.Printf("Where Num=0 :\t %+v\n", result)

	// 期待する発行SQL: Select * From samples Where Bool = 0
	db.Where(&Sample{Bool: sql.NullBool{Bool: false, Valid: true}}).Find(&result)
	fmt.Printf("Where Bool=0 :\t %+v\n", result)

	// 期待する発行SQL: Select * From samples Where Str = ""
	db.Where(&Sample{Str: sql.NullString{String: "", Valid: true}}).Find(&result)
	fmt.Printf("Where Str=\"\" :\t %+v\n", result)
}
```

```
Where Num=0 :	 [{Num:{Int64:0 Valid:true} Bool:{Bool:false Valid:true} Str:{String: Valid:true}}]
Where Bool=0 :	 [{Num:{Int64:0 Valid:true} Bool:{Bool:false Valid:true} Str:{String: Valid:true}}]
Where Str="" :	 [{Num:{Int64:0 Valid:true} Bool:{Bool:false Valid:true} Str:{String: Valid:true}}]
```

結果は想定通り、きちんとWhereで検索条件を絞り込めています。
pointerを使う案よりは値を使いやすく書けると思います。
毎回 `Valid: true` を設定するのは面倒なので、Helperを用意するのが良さそうです。


# 結論

* gorm を使う前には事前に公式Docの**NOTE**の部分をよく読むこと
* gorm で 値にGoのゼロ値を使う可能性がある場合は 解決策1～3 のような工夫が必要
* 個人的には[解決策3](#解決策3-sqlnull-typeを使用する)が良さそう