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の世界を広げましょう。