生涯未熟

生涯未熟

プログラミングをちょこちょこと。

guregu/nullでぬるぬるする

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 を使った方法を使うのがベターでしょう。