SQL関連のGoライブラリー紹介

  • 28
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

GunosyのGoエンジニア、Gregと申します。GoConなどでお会いしたことがあるかもしれません。1日目の記事で松本さんがいくつかのライブラリーを紹介しましたが、もう少し詳しく説明しようと思っています。日本語不自由で申し訳ないです!

SQLのデータ型

API開発での多くのフローはこういう感じだと思います。

  • DBからデータを取る
  • Goが扱えるstructなどにデータを読み込む
  • データを編集してDBに保存
  • データをJSONにしてクライアントに送る

データがシンプルな形であれば、標準パッケージだけで済みますが、たとえばNULL可の値などが入ると、扱いが若干面倒くさくなるので、以下のライブラリーを作りました。

guregu/null

https://github.com/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

https://github.com/guregu/toki

tokiはSQLのTIME型を対応してくれるとても小さなライブラリーです。toki.TimeとNULL許容のtoki.NullTimeがあります。guregu/nullのように、structに入れるだけどDBとJSONのやりとりがシンプルになります。

なぜ作ったのか

time.Timeも文字列も使えますが、time.Timeはタイムゾーンなど、複雑なデータが含まれたため、バグがちょいちょい出ていました。文字列はできるだけテキスト以外に使いたくないですね。

SQLのテスト

guregu/mogi

https://github.com/guregu/null

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