読者です 読者をやめる 読者になる 読者になる

Datastore/Go のデータ設計と struct の振る舞いについて

golang tokyo #5 の LT資料です。
golangtokyo.connpass.com



以下のような感想をいただいたり・・・







以下の様な指摘をいただきました。
とても勉強になりました。


Datastore/Go で datastore: unsupported struct field type: xxx のエラー

以下の struct を Datastore に put しようとしたら・・・

type User struct {
    _kind   string  `goon:"kind,User"`
    ID      string
    Tasks   []*Tasks
}

type Task struct {
    Title   string
}



以下のエラーが発生した。

datastore: unsupported struct field type: *Task

原因は []*Tasks のようにポインタを指定しているから、
まあ、Datastore に保存するので値を渡さないとね・・・。

Datastore/Go の Get(), GetMulti() で指定した key が存在しない場合はエラーになる

ちょっとまとめておこうかと。

間違っているところがあったらブログのコメント or twitter で教えてください。
(´・ω・`)

Datastore では Get() に指定した key の entity が存在しない場合、
datastore.ErrNoSuchEntity というエラーが発生する。
*データが存在しないとエラーになるのはちょっとビックリしました。

なので、以下のようなエラーハンドリングは正常に動作しない。
entity が存在しない場合は datastore.ErrNoSuchEntity が発生するので、
最初の if err != nil で引っかかってしまう。
if entity == nil には到達しない。

err := datastore.Get(ctx, key, &entity)

if err != nil {
    //Datastoreでエラーが発生した場合
    return
}

if entity == nil {
    //entityが存在しない場合
    return
}

//entityが存在する場合
return



以下のように datastore.ErrNoSuchEntity を利用して捕捉するのもダメ。
最初の err != nil で datastore.ErrNoSuchEntity を捕捉してしまう。

err := datastore.Get(ctx, key, &entity)

if err != nil {
    //Datastoreでエラーが発生した場合
    return
}

if entity == datastore.ErrNoSuchEntity {
    //entityが存在しない場合
    return
}

//entityが存在する場合
return



以下のように先に datastore.ErrNoSuchEntity をチェックする必要がある。
*このコードだけ見ると、最初に if err == nil でチェックしてもいーんだけど。

err := datastore.Get(ctx, key, &entity)

if entity == datastore.ErrNoSuchEntity {
    //entityが存在しない場合
    return
}

if err != nil {
    //Datastoreでエラーが発生した場合
    return
}

//entityが存在する場合
return



ちなみに以下のエラーは if の等号比較で判別可能。
https://github.com/golang/appengine/blob/master/datastore/datastore.go#L20-L28

そして、GetMulti() でも同じように datastore.ErrNoSuchEntity は発生する。

ただ、面倒なことに XxxMulti() 系のAPI
戻り値の error が appengine.MultiError という特殊な error になる。
https://cloud.google.com/appengine/docs/standard/go/datastore/reference#hdr-Basic_Operations

GetMulti, PutMulti and DeleteMulti are batch versions of the Get, Put and Delete functions. They take a []*Key instead of a *Key, and may return an appengine.MultiError when encountering partial failure.



appengine.MultiError は複数の error を扱うためのもので、
実体は error の slice になっている。
https://github.com/golang/appengine/blob/master/errors.go#L25


で、appengine.MultiError の error interface が以下。
https://github.com/golang/appengine/blob/master/errors.go#L27-L46


Multi系APIは発生したエラー件数によって Error() で返す文字列が変わる。
https://github.com/golang/appengine/blob/master/errors.go#L37-L45


エラーが1件だけだと、
その1件の Error() をそのまま返すので、
ローカル環境でテキトーにデバッグしてて、
エラーメッセージだけ確認してると
appengine.MultiError が返ってきてるはずなのに
単に datastore.ErrNoSuchEntity が返ってきてると思ってしまうことがあるかもしれない。
まあ、ないと思うけど。


GetMulti() で datastore.ErrNoSuchEntity が発生した場合、
appengine.MultiError は以下のような構造になる。

appengine.MultiError {
    datastore.ErrNoSuchEntity, 
}

1件でも複数件でも実体が []error であることは変わらない。


ということで、
複数指定した key のうち、
いずれかが存在しなくても正常系だとみなす場合は
以下のようにループで error をチェックして、
datastore.ErrNoSuchEntity をエラーと見なさない実装にする必要がある。

if err := datastore.GetMulti(ctx, keys, &entities); err != nil {
    mErr := err.(appengine.MultiError)
    for _, e := range mErr {
        if e == nil {
            //entity が存在する
            continue
        }
        
        if e == datastore.ErrNoSuchEntity {
            //entityが存在しないけど正常系とみなすのでスルー
            continue
        }
        
        //ここまで来ると datastore.ErrNoSuchEntity 以外のエラー
    }
}



ここでポイントになるのが最初の if e == nil のチェック。
これはなくてもよさそうだが、
appengine.MultiError の実体である error は key の数だけ要素数が作られるみたいなので、
そうもいかない。


例えば GetMulti() で key = 1,2,3 を指定して、
key = 1 だけ datastore.ErrNoSuchEntity が発生すると、
以下のような
error になる。
エラーじゃない場合は nil で埋まる。

[]error {
    appengine.ErrNoSuchEntity, //entityがない
    nil, //entityある
    nil, //entityある
}



これを for で回すので、if err == nil のチェックをしないといけない。

*if条件の書き方によっては err == nil のチェック不要になりますが、
 nilで埋まる的な説明したかったので・・・。

以下のように Get() と同じようにチェックしてはいけないので、
Multi系API は少し面倒ですね。

if err := datastore.GetMulti(ctx, keys, &entities); err != nil {
    if err == datastore.ErrNoSuchEntity {
        //appengine.MultiError なので、datastore.ErrNoSuchEntity で捕捉できない
    }
}



GetAll() で Query を指定した検索では datastore.ErrNoSuchEntity が発生しません。
ハンドリング不要です。

ちなみに、Datastore のアクセス部分を抽象化する場合、
datastore.ErrNoSuchEntity をそのまま返すと Datastore に依存してしまうので、
nil を返すようにするか、
アプリケーション独自のエラーを定義してあげて、
クライアントコードではそれをハンドリングしてあげる方がいいと思います。

Goのシンプルさについて

LT資料です。

golang + mysql の ORM を色々調べた感想とおすすめ

普段は GAE で golang を使っているけど、
golangmysql 使ったことないってのもどうなのかな? と思ったので、
ちょっとしたサンプルアプリを通して mysql を使ってみようと思った。
で、ORM どーしーよーかなと思って、
色々調べた記録です。

標準パッケージ

標準パッケージが良い感じであれば、ORM は不要だと思ったので確認してみた。
標準パッケージでは database/sql を利用する。
https://golang.org/pkg/database/sql/

以下が使用例
https://github.com/golang/go/blob/master/src/database/sql/example_test.go

生のSQLを実行するのは問題ないんだけど、
Scan() で値を取り出していくのが面倒。

以下のように結果が複数レコードの場合に for で回すのも面倒。
https://github.com/golang/go/blob/master/src/database/sql/example_test.go#L77-L86

あと、struct とのマッピングは欲しい。

ということで、要件にもよるけど、
database/sql をそのまま利用するのはちょっと厳しいかな。

ちなみに、database/sql と database/sql を利用している ORM を利用するには
以下のドライバが必要になる。
https://github.com/golang/go/wiki/SQLDrivers

ドライバとdatabase/sqlの関係性は以下を読むとイメージできるかもしれない。
http://pospome.hatenablog.com/entry/2017/01/29/171904


ORMに求める要件

ORMを探すにあたって、個人的に以下の要件は必須。

structにマッピングできる

これは欲しい。
struct の tag とかでカラム指定したりするイメージ。

テーブル名とかstructに変な命名規則を適用させる必要がない

usersテーブルに対する struct は Users にする必要がある的なルールがあってもいーんだけど、
それを struct の tag とか、interface とかで回避できる仕組みは欲しい。

SQLが書ける

複雑なSQLをクエリビルダとかで書くのが面倒だったり、
ORMがSQLの方言に対応してなかった時(mysql5.7のjson系の関数とか)に生SQLで書きたい。
その結果はもちろん struct にマッピングしたい。



以下は必須じゃないけど、気にするところ。

発行されるクエリがイメージできる

「裏側で妙なSQLが発行されてインデックスが効かない」
「意図しないSQLが発行されている」
みたいなORMあるあるは回避したい。
association みたいなテーブルを良い感じに join する機能を明示的に利用しない限りこーゆーことはないと思うから、
そんなに気にする必要もないかもしれない。

パフォーマンスが極端に悪くない

早いに越したことはないが、どこまで速度にこだわるのかって問題になるから、
正直どーでもいいというか、使い勝手とのトレードオフ
どーしてもパフォーマンスが気になるなら、標準パッケージの database/sql 使えばいいと思うし、
どーでもいいなら、使い勝手を重視すればいい。



色々調べたが、これらを満たさないORMは基本なかったと思う。
なので、最終的には好みの問題になるのかもしれない。

ただ、パフォーマンスは実際に測定してないので分からない。
ネットに比較結果が転がっていたりするので、
気になるようであれば調べてみるといいかもしれない。

gorm

最初に調べたのが gorm というORMだった。
https://github.com/jinzhu/gorm

ドキュメントは以下。
http://jinzhu.me/gorm/

他のORMに比べて多機能な印象を持った。
associations とかもある。
http://jinzhu.me/gorm/associations.html

これを使っておけば変に困ることもないと思う。

ちなみに、gorm を fork した ngorm というのがあるが、
現時点で mysql に対応していないので見送り。
https://github.com/ngorm/ngorm

gorp

次は gorp を調べた。
https://github.com/go-gorp/gorp

「SELECTでは生SQLを書く必要がある」という点は独特だなと思った。

以下のようなイメージで、生SQLを書いて、それに struct をバインドする。
https://github.com/go-gorp/gorp/blob/master/gorp_test.go#L859

insert, update, delete は他のORMと同じ印象。
insert
https://github.com/go-gorp/gorp/blob/master/gorp_test.go#L432
update
https://github.com/go-gorp/gorp/blob/master/gorp_test.go#L564
delete
https://github.com/go-gorp/gorp/blob/master/gorp_test.go#L653

もう1点独特だなと思ったのは struct と DBテーブルのヒモ付を struct の tag, interface ではなく、
以下のようにコードで指定するところ。
struct が tag, interface で汚れないのは嬉しい。
https://github.com/go-gorp/gorp/blob/master/gorp_test.go#L1227-L1228

問題は SELECT WHERE IN() が使えない点。

使えないことはないけど、
gorpではSELECTをSQLで書く必要があるので、
以下のようにプレースホルダーを利用したSQL
そのプレースホルダーに対応する 100, 200, 300 という値をバインドしていかないといけない。

err = dbmap.Select(&users, "select * from posts where id in (?, ?, ?)", 100, 200, 300)



そして、以下のように slice で指定できないのがキツイ。
slice を指定するとエラーになる。

err = dbmap.Select(&users, "select * from posts where id in (?, ?, ?)", []int{100,200,300})



これどーやって in で指定するパラメータを可変にするんでしょうね・・・。
issue にもなっているが、解決されていない・・・。
https://github.com/go-gorp/gorp/issues/85
自分はここが気になって gorp の利用を見送った。

dbr

https://github.com/gocraft/dbr

これは以下の記事を見た方が早い。
https://eurie.co.jp/blog/engineering/2015/12/go-lang-ormapper-dbr

自分の要件を満たしていて良い感じだったが、なぜか timezone が UTC から JST に変更できなかった。

dbrというか、database/sql で利用するドライバのレイヤの話になると思うんだけど、
以下のように設定すれば timezone を変更可能なはずが、なぜか反映されない・・・。
https://note.mu/tomyhero/n/nc31c788bc7d8
http://kenzo0107.hatenablog.com/entry/2015/08/19/165310

他の ORM と比較して公式のドキュメントが弱い印象も受けた。
というか、他が頑張ってドキュメント作ってる気がする。

JST問題は頑張って解決してもよかったが、一旦他のORMを調べることにした。

xorm

最後に試したのがこれ。
https://github.com/go-xorm/xorm

ドキュメントは以下。
http://xorm.io/docs/

エウレカさんで採用されているらしい。
https://developers.eure.jp/tech/go_web_application_1/

インメモリキャッシュを備えているのも珍しい。
実用に耐えるかは不明。

面白いと思ったのは
xormを利用すると wizard というシャーディング&レプリケーション用のライブラリも利用できる。
https://github.com/evalphobia/wizard
ちゃんと見てないし、使ってないから分からないんだけど、
生のSQLやクエリビルダは書けないっぽい????
https://github.com/evalphobia/wizard/blob/master/orm/xorm/interface.go#L13-L54

wizard に関しては、実際どの程度実用に耐えるのか不明。


おすすめは?

おすすめとしては gorm かな・・・。
パフォーマンス面や使い勝手(これは個人差あるけど)は他の ORM に劣るかもしれないが、
多機能なので「あれができない」「これができない」で困ることはないし・・・。
gorm が合わなかったら、他の ORM を使ってみればいいと思う。


結局何を使ったのか?

なんとなく最後の xorm を使うことにした。
今のところ、大きな問題もなく使えている。
素直に gorm にしておけばよかったのかもしれない・・・。

そーいえば、PHP の Cake, Fuel, rubyrails のようなフルスタックなWAFは
それ用のORMが付属してるから、
こーやって色々調べる必要なくて楽だったな・・・。