31
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

GunosyAdvent Calendar 2014

Day 12

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

Last updated at Posted at 2014-12-12

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

31
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?