Goでサーバーサイドの開発を行っているとMySQLなどのRDBに接続して、取得した値をJSONで返す機会は多いはず。
そして大概ハマるのが「NULLを許容してJSONで返すのはどうやればいいんだ・・・」というところでしょう。
今回はそんな話をします。
ノーガード戦法
まずは特に何もしなかったパターンを見てみましょう。
テストデータは以下のようなものを使います。
+-------+------+
| name | age |
+-------+------+
| tarou | 12 |
| NULL | NULL |
| | 0 |
+-------+------+
適当な値が入ったレコードと、NULLが入ったレコードと、空文字・0が入ったレコードになります。
これを以下のようなコードで使ってみましょう。
package main
import (
"database/sql"
"encoding/json"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
type User struct {
Name string
Age int
}
func main() {
db, err := sql.Open("mysql", "root:@/test")
if err != nil {
panic(err)
}
defer db.Close()
rows, err := db.Query("SELECT * FROM user")
if err != nil {
return
}
for rows.Next() {
var user User
rows.Scan(&user.Name, &user.Age)
JSON, err := json.Marshal(user)
if err != nil {
continue
}
fmt.Println(string(JSON))
}
}
これを動かしてみると・・・
{"Name":"tarou","Age":12}
{"Name":"","Age":0}
{"Name":"","Age":0}
おっ、NULLが空文字と0になりましたねー。
このように特に何もしなかった場合は型に応じた初期値になるんですよね。
王道パターン
さて、ここで「ちゃんとNULLで出力出来ないと困るよ!」って場合には以下のようにUser構造体の型を変えることで判別できます。
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"github.com/k0kubun/pp"
)
type User struct {
Name sql.NullString
Age sql.NullInt64
}
func main() {
db, err := sql.Open("mysql", "root:@/test")
if err != nil {
panic(err)
}
defer db.Close()
rows, err := db.Query("SELECT * FROM user")
if err != nil {
return
}
for rows.Next() {
var user User
rows.Scan(&user.Name, &user.Age)
pp.Println(user)
}
}
これを実行すると
{"Name":{"String":"tarou","Valid":true},"Age":{"Int64":12,"Valid":true}}
{"Name":{"String":"","Valid":false},"Age":{"Int64":0,"Valid":false}}
{"Name":{"String":"","Valid":true},"Age":{"Int64":0,"Valid":true}}
のようになります。
Valid
というのがNULLかどうかのbool値を持っている構造体が含まれているのが分かりますね。
このままでは使い物にならないので、良い感じにNULLを返すために新たに sql.NULLXXX
を含んだ構造体を作成して、ゴニョゴニョやるパターンが多く見受けられます。
package main
import (
"database/sql"
"encoding/json"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
type (
User struct {
Name NullString `json:"name"`
Age NullInt64 `json:"age"`
}
NullString struct {
sql.NullString
}
NullInt64 struct {
sql.NullInt64
}
)
func (ns NullString) MarshalJSON() ([]byte, error) {
if !ns.Valid {
return []byte("null"), nil
}
return json.Marshal(ns.String)
}
func (ni NullInt64) MarshalJSON() ([]byte, error) {
if !ni.Valid {
return []byte("null"), nil
}
return json.Marshal(ni.Int64)
}
func main() {
db, err := sql.Open("mysql", "root:@/test")
if err != nil {
panic(err)
}
defer db.Close()
rows, err := db.Query("SELECT * FROM user")
if err != nil {
return
}
for rows.Next() {
var user User
rows.Scan(&user.Name, &user.Age)
JSON, err := json.Marshal(user)
if err != nil {
continue
}
fmt.Println(string(JSON))
}
}
MarshalJSON() ([]byte, error)
を持つ構造体を作成することで、 Marshaler
Interfaceを満たし json.Marshal
実行時に定義した MarshalJSON
を実行してくれます。
これを実行した結果は・・・
{"name":"tarou","age":12}
{"name":null,"age":null}
{"name":"","age":0}
ちゃんとnullが許容されてますね!
さて、これで当初の目的は達成できたわけですが、いちいち sql.NULLXXX
を含んだ構造体を作成して MarshalJSON
を定義するのはヒジョーに面倒です。
そこで、 guregu/null
の出番です。
guregu/null
使い方は非常に簡単。
NULL値が入った時に null.XXX
型にはnullを、 zero.XXX
型には型の初期値に変換されます。
package main
import (
"database/sql"
"encoding/json"
"fmt"
_ "github.com/go-sql-driver/mysql"
"gopkg.in/guregu/null.v3"
"gopkg.in/guregu/null.v3/zero"
)
type (
User struct {
Name null.String `json:"name"`
Age null.Int `json:"age"`
}
)
func main() {
db, err := sql.Open("mysql", "root:@/guregu")
if err != nil {
panic(err)
}
defer db.Close()
rows, err := db.Query("SELECT * FROM user")
if err != nil {
return
}
for rows.Next() {
var user User
rows.Scan(&user.Name, &user.Age)
JSON, err := json.Marshal(user)
if err != nil {
continue
}
fmt.Println(string(JSON))
}
}
こうすることで
{"name":"tarou","age":12}
{"name":null,"age":null}
{"name":"","age":0}
ちゃんと null.XXX
はnullに、 zero.XXX
は型の初期値になってますね。
単純にnullにしたい場合は便利なライブラリですね。
ただ、UnmarshalやMarshal時に特殊な処理をしたい場合は sql.NULLXXX
を使った方法を使うのがベターでしょう。