GunosyのGoエンジニア、Gregと申します。GoConなどでお会いしたことがあるかもしれません。1日目の記事で松本さんがいくつかのライブラリーを紹介しましたが、もう少し詳しく説明しようと思っています。日本語不自由で申し訳ないです!
SQLのデータ型
API開発での多くのフローはこういう感じだと思います。
- DBからデータを取る
- Goが扱えるstructなどにデータを読み込む
- データを編集してDBに保存
- データをJSONにしてクライアントに送る
データがシンプルな形であれば、標準パッケージだけで済みますが、たとえばNULL可の値などが入ると、扱いが若干面倒くさくなるので、以下のライブラリーを作りました。
guregu/null
SQLのNULL許容型をJSONに吐き出してるなら幸せになるライブラリーです。nullとzeroというパッケージがあって、nullはnull値とzero値を別々に扱って、zeroはゼロ値とnull値を一緒にします。好きなstructに入れるだけでこういう感じになります。
type ImportantData struct {
NullyString null.String
ZeroingString zero.String
}
JSONエンコード
DBでの値 |
null.Int のJSON |
zero.Int のJSON |
---|---|---|
NULL | null | 0 |
0 | 0 | 0 |
123 | 123 | 123 |
DBでの値 |
null.String のJSON |
zero.String のJSON |
---|---|---|
NULL | null | "" |
"" | "" | "" |
"hello" | "hello" | "hello" |
DBに保存すると…
値 | null.Int |
zero.Int |
---|---|---|
NULL | NULL | NULL |
0 | 0 | NULL |
123 | 123 | 123 |
値 | null.String |
zero.String |
---|---|---|
NULL | NULL | NULL |
"" | "" | NULL |
"hello" | "hello" | "hello" |
なぜ作ったのか
sql.NullInt64
など、標準パッケージにありますが、JSONエンコードすると、こうなります。
{
"myNullableInt": {
"Int64": 1234,
"Valid": true
}
}
使い物にならないですね。ポインターを使って*int
にしたら綺麗にJSONエンコードされますが、invalid memory address or nil pointer dereference
が出やすくなり、扱いが面倒になります。
guregu/toki
tokiはSQLのTIME型を対応してくれるとても小さなライブラリーです。toki.Time
とNULL許容のtoki.NullTime
があります。guregu/null
のように、structに入れるだけどDBとJSONのやりとりがシンプルになります。
なぜ作ったのか
time.Time
も文字列も使えますが、time.Time
はタイムゾーンなど、複雑なデータが含まれたため、バグがちょいちょい出ていました。文字列はできるだけテキスト以外に使いたくないですね。
SQLのテスト
guregu/mogi
SQLサーバーを立てずにテストができる、SQLのモックドライバーです。たとえば、このコードとテストがあるとします。
モデル
var db *sql.DB
type Beer struct {
ID int64
Name string
Pct float32
}
func GetBeer(id int64) (beer Beer, err error) {
query := `SELECT id, name, pct FROM beer WHERE id = ?`
err = db.QueryRow(query, id).Scan(&beer.ID, &beer.Name, &beer.Pct)
return
}
テスト
var beerFixture = Beer{
ID: 42,
Name: "Yona Yona Ale",
Pct: 5.5,
}
func TestGetBeer(t *testing.T) {
beer, err := GetBeer(42)
if err != nil {
t.Fatal("err should be nil, but is:", err)
}
// Here's a lazy way to compare our results and our expectations.
if !reflect.DeepEqual(beer, beerFixture) {
t.Errorf("%#v ≠ %#v", beer, beerFixture)
}
}
SQLサーバーに接続して、beerFixture
のデータを入れれば通りますが、モックしてみましょう。
まず、mogiに"接続"しましょう。
import (
"database/sql"
"github.com/guregu/mogi"
)
func init() {
db, _ = sql.Open("mogi", "")
mogi.Verbose(true) // stubされてないクエリーをログ
}
そして、stubを入れましょう。
func TestGetBeer(t *testing.T) {
defer mogi.Reset() // 最後にReset()しないとstubが残されてしまう
mogi.Select("id", "name", "pct").StubCSV(`42,Yona Yona Ale,5.5`)
...
}
mogi.Select(...)
にstubしたいコラムを渡します。しかし、id = 42
以外のレコードや、beer
以外のテーブルのレコードに反応してしまいますので、もう少し精密にしましょう。
mogi.Select("id", "name", "pct").
From("beer").
Where("id", 42).
StubCSV(`42,Yona Yona Ale,5.5`)
完璧です。これでいいテストが書けました。
SELECT以外のSQL文や非同期処理対応など、いろいろできるので、ぜひ試してみてください。
最後に
ここまで読んでくれてありがとうございます。プルリクエストはいつでも受けますので、一緒にGoとSQLの世界を広げましょう。