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が付属してるから、
こーやって色々調べる必要なくて楽だったな・・・。

(継続的に追記)Datastore/Go のデータ設計のコツ

Datastoreを使っていて、
ある程度コツとか注意点みたいなものが分かってきたので、
まとめてみました。
継続的に追記していく予定です。

間違っているところがあれば
コメント or twitter で教えてください。

Datastoreの entity, kind などの用語は理解している前提です。

ParentKeyに気をつける

外部キーのノリで利用してはいけない。
詳しくはこちら。
http://pospome.hatenablog.com/entry/20161009/1475990332


GO では Filter による OR, IN 検索ができない

Keyでの GetMulti() は可能だが、
それ以外の property で OR, IN による検索はできない。
https://cloud.google.com/appengine/docs/go/datastore/queries#filters

以下のようにループで回す必要がある。

var users []User
for _, name := range names {
    var tmp []User
    q := datastore.NewQuery("User").Filter("name =", name)
    if err = q.GetAll(ctx, &tmp); err != nil {
        //error
    }
    users = append(users, tmp)
}

ちなみに、Java, Pythonでは利用できる。
https://cloud.google.com/appengine/docs/java/datastore/queries#filters
https://cloud.google.com/appengine/docs/python/datastore/queries#filters

地味に「!=」も利用できない。
これはDatastoreがindexベースの検索を提供しているので、当然といえば当然だけど。


文字列に対する LIKE 検索がない

これもない。


結局どんなクエリが発行できるのか?

Datastore には IN, OR, LIKE がないことを説明したが、
結局何ができて、何ができないのかが気になると思う。

以下の関数一覧を見れば大体分かる。
https://cloud.google.com/appengine/docs/standard/go/datastore/reference#index

COUNT, DISTINCT, LIMIT, OFFSET, ORDER は利用できる。

注意したいのは Filter() で指定できる検索条件。
https://cloud.google.com/appengine/docs/standard/go/datastore/reference#Query.Filter
Datastore Queries  |  App Engine standard environment for Go  |  Google Cloud Platform


あとは Cursor というページングに利用できる機能もある。
https://cloud.google.com/appengine/docs/go/datastore/reference#Cursor

SUM() などの集計系が弱い(というかない)ので、
集計結果が欲しい場合はDatastoreのデータをBigQueryに突っ込むなり、
Datastoreのデータに対して集計処理をしてあげて、サマリ用の kind に突っ込む、
的なことが必要になる。


SearchAPIによる柔軟な検索

RDBに比べると、どうしても検索の柔軟性が低くなってしまうが、
SearchAPIを利用することで柔軟な検索が可能になる。
http://mame0112.hatenablog.com/entry/2015/05/31/114057

ただ、SearchAPI は Datastore 内のデータについて検索をかけることはできないので、
Datastoreと同じデータを SearchAPI にも用意する必要があるっぽい。
http://gcp-memo.nomiso.net/2015/12/datastore-serach-api.html


structのネストによってValueObjectを利用する

Datastore に保存する entity の定義は struct を利用するが、
以下のように struct をネストすることができる。

type User struct {
    Name        string
    Profile     Profile
}

type Profile struct {
    Tel        string
    Address    string
}



entity の property は User.Name, User.Profile.Tel, User.Profile.Address のようになり、
これらの値は entity を取得したときに User, Profile それぞれの struct にバインドされる。

とはいえ、以下のようにフラットにするのとあまり変わりない気がする。

type User struct {
    Name       string
    tel        string
    address    string
}



struct をネストすると何が嬉しいのかというと、
アプリケーション側のコードに関係してくる。

DatastoreはJOINができないので、
1つのentityが持つ情報量が多くなる傾向にある。

全ての property をフラットに並べてしまうと
1つの巨大なstructに振る舞いを持たせることになるので、
その struct が持つ責務が大きくなってしまう。

struct を適切な粒度で管理しておけば(ネストさせておけば)、
こういった責務の肥大化を防ぐことができる。
なので、Datastoreというよりは、
設計ベースで考えてネストさせるかどうかを決めればいい。

struct のネストについてはこちらも参考にするといい。
http://qiita.com/hogedigo/items/463d71715fa14a5d5fd3

ネストを利用した設計についてはこちらをどーぞ。
http://pospome.hatenablog.com/entry/2017/04/28/000625

ちなみに、以下を利用すると
Datastoreから entity を get するタイミングと
put するタイミングをフックできるので、
Datastore のデータ構造と struct に乖離があっても、
手動でバインドさせることができる。
https://cloud.google.com/appengine/docs/go/datastore/reference#PropertyLoadSaver


primitive type は後からでも利用可能。

以下の string の Name を

type User struct {
    Name        string `datastore:"name"`
}

以下のように primitive type にした Name に変更しても、
エラーになることはない。

type Name string

type User struct {
    Name        Name `datastore:"name"`
}

まあ、これはどっちも string だから当然といえば当然。


map は保存できないが、sliceは保存できる

https://cloud.google.com/appengine/docs/go/datastore/reference#hdr-Basic_Operations

ここに載っているように int, string などの基本的な型はもちろん、
struct の slice まで保存できる。
でも、map は保存できない。(´・ω・`)

map を保存したい場合は
以下のような key, value を持った struct を slice で突っ込めばいいと思う。

type MyStruct struct {
    key string
    value string
}

以下にも注意。
http://pospome.hatenablog.com/entry/2017/02/05/164525
http://pospome.hatenablog.com/entry/2017/04/20/172148



slice を検索できる

[]int のような slice の property は filter 可能。
struct の slice も struct の property で filter 可能。


制限事項と保存できる文字列の最大長

https://cloud.google.com/datastore/docs/concepts/limits

こちらに制限事項が載っている。
entity のサイズ制限とかあるので、目を通すといい。

個人的に気になったのは
「インデックス付けされた文字列プロパティの UTF-8 エンコードの最大サイズ」

「インデックス付けされていないプロパティの最大サイズ」
のところ。

string の property に index を貼ると max 1500 byte だが、
index を貼らなければ max 1048487 byte まで保存できる。

1500 byte 以上の json などを保存する場合は
noindex を指定する必要がある。

index を貼らないとその property を検索できなくなってしまうが、
Datastore は string に対する LIKE 検索を提供していないので、
1500byte 以上の json を完全一致で検索することがない限り、
noindex でも問題ないと思う


key以外にユニークなフィールドを持つのは面倒

ユニークな property は key にした方がいい。
以下はDatastoreのエラーの記事だが、
例としてユニークな property の実現方法について説明している。
http://pospome.hatenablog.com/entry/20161108/1478611356

とはいえ、ユニークにしたい値が key として相応しいとは限らない。

例えば、email はユニークにしたいことが多いと思うが、
この場合、pospome@email.com のような記号を含んだ文字列が key になってしまう。
@, - などの記号が key に含まれるのを避けたい場合は
email をハッシュ化したものを key にするといい。
文字列にはなるが、key に記号が含まれるよりはいくらかマシかもしれない。

複合ユニーク制約を持たせるのも面倒なので、
ユニークな組み合わせになる property をハッシュ化して key にした方がいい。
(別にハッシュ化せずに property1-property2 とかでもいーんだけど)

ユニーク制約については以下が参考になる。
http://d.hatena.ne.jp/higayasuo/20091111/1257905482


JOINできないので、実データを突っ込むと良い

Datastoreに限らず、KVSは JOIN ができないことが多いので、
kind 同士でリレーションを貼らずにそのまま値を持った方がいい。

例えば、user が複数の task を保つ場合、
user kind と task kind を用意して、
taks kind に user kind の key を持たせて外部キーみたいなリレーションを貼るのではなく、
task の slice を user kind に突っ込む。

ただし、entity のサイズには制限があるので、
user が保持する task の個数多すぎて、そのサイズ制限を超える場合は別 kind にする必要がある。


Datastore の property が struct の field に存在しないとエラー

以下のような struct があって、

type User struct {
    Name string
    Score int
}

Datastore の property が Name, Score, Address で、
struct の field に Address が存在しない場合、
datastore.ErrFieldMismatch が発生する。

エラーメッセージでいうと以下。
「datastore: cannot load field xxxx into a xxx: no such struct field」

逆に struct の field が Datastore の property よりも多い場合は struct の field が zero value になるだけで、
エラーは発生しない。

Datastoreのスキーマを変更する際は
先に struct に field 定義を追加したコードをデプロイしてから
Datastore を触るコードをデプロイするという方法も選択肢の1つとして有効かもしれない。

ただ、エラーが発生してもエラー対象以外のフィールドには値がバインドされるので、
datastore.ErrFieldMismatch をハンドリングして無視してしまえば、
上記のようにデプロイを工夫する必要もない。

ちなみに、Goon という Datastore のラッパーを利用することで
これを回避できます。
http://pospome.hatenablog.com/entry/2017/07/19/213944




propertyのno-indexに注意

詳しくはココ。
https://cloud.google.com/datastore/docs/concepts/indexes#unindexed_properties

type User struct {
    Name string `datastore:"name,noindex"`
}

上記のように検索条件にならない property には noindex を指定してもいいが、
後から必要になって noindex を外した場合、
既存の entity に index が貼られることはない。

noindex を外した後に put された entity にのみ index が貼られる。
インデックスについてはしっかりと学んでおく必要がある。
https://cloud.google.com/datastore/docs/concepts/indexes


複合インデックス or 仕様割り切り or コードでフィルタリング

Datastore は filter で「検索条件指定」と「ソート」を指定すると、
複合インデックスが必要になる。
https://cloud.google.com/datastore/docs/concepts/indexes#index_definition_and_structure

複合インデックスがあまりにも増える場合は
「desc はサポートするけど、asc はサポートしない」
「指定する検索条件を限定して、アプリケーションコードでフィルタリングする」
といった割り切りが必要になるかもしれない。

アプリケーションコードでフィルタリングする場合に注意したいのが、
ページングの実装。

アプリケーションコードでフィルタリングした結果、
1ページに表示する件数に満たない可能性を考慮して、
entity を多めに取得したり、
各ページの最後に表示されている entity の id を管理しないといけなかったり、
意外と面倒になることが多い。

なので、
ページングが必要な場合は可能な限り 複合インデックス + Cursor を利用した方がいい。

ちなみに、1つのプロジェクトで定義できる複合インデックスには上限がある。
現在は 200 が上限。


エンティティグループは1秒間に1回しか更新できない & 解決策

以下によると、「エンティティグループへの最大書き込み速度」は「毎秒1回」に制限されている。
https://cloud.google.com/datastore/docs/concepts/limits

ParentKey(ParentEntity)を持たないエンティティも1つのエンティティグループとみなされるので、
毎秒1回しか書き込みできない。

例えば、
ホームページのアクセスカウンターを以下のような1つの entity で実現しようとした場合、

{
    id int
    access_count int
}

access_count のインクリメントは1秒間に1回しかできないので、
ホームページに秒間1アクセス以上のアクセスがあった場合、
このアクセスカウンターは機能しない。

ただ、自分は1秒に1回以上書き込んだことがないので、
「インクリメントが即時反映されないけど、書き込み操作自体は可能」なのか、
「書き込み操作自体不可能」なのかは不明。




これを解決する方法として、以下がある。
https://cloud.google.com/appengine/articles/sharding_counters

この ShardingCounters というテクニックの仕組みは単純で、
entity を複数個用意してランダムでインクリメントして、
それらの entry のカウントを合計すればいいというもの。
自分は元々ソーシャルゲームを運用していたが、
MySQLでこういった実装をしたことがある。

it is important to note that you can only expect to update any single entity or entity group about five times a second.

ちなみに、さきほど「秒間1回しか書き込みできない」と書いたが、
この記事によると、1秒間に5回までしか entity を update できないと書いてある。
どちらが正しいかは不明。


トランザクションは楽観ロック

Datastoreにもトランザクションは存在するが、楽観ロックになっている。

仕組みとしては、
各 RootEntity は最終更新時間を持っていて、
その更新時間がトランザクションのコミット時間よりも前であればコミットできる。
後であれば、ロールバックしてリトライするというもの。

RootEntity の最終更新時間がベースになるので、
巨大な entityGroup かつ、更新頻度が高いものは、
トランザクションでリトライが走る可能性が高くなる。

以下を true にすると複数の kind にまたがってトランザクションをかけることができる。
https://cloud.google.com/datastore/docs/concepts/transactions

トランザクションはここが詳しい。
http://www.apps-gcp.com/datastore-transaction/
https://cloud.google.com/datastore/docs/concepts/transactions


トランザクションでは冪等性を担保する必要がある

Datastoreはトランザクションが失敗するとリトライする。
リトライ回数は以下で設定できる。
https://github.com/golang/appengine/blob/master/datastore/transaction.go#L84-L86

ここでポイントになるのが RunInTransaction() 内の操作は冪等性を担保する必要があるということ。

以下のように RunInTransaction() 内で score++ してしまうと、
Put() が失敗して、
RunInTransaction() 内がリトライされるので、
score は 前回の 101 をインクリメントして 102 になってしまう。

score := 100
RunInTransaction(ctx, func(tx *datastore.Transaction) error {
    score++
    _, err := tx.Put( "key", &User{score})
   return err
})



以下のようにする必要がある。
RunInTransaction() の外で値を確定させておいた方が良い。

score := 100
score++
RunInTransaction(ctx, func(tx *datastore.Transaction) error {
    _, err := tx.Put( "key", &User{score})
   return err
})



ちなみに、Goon を利用すると、struct の key を自動的に埋めてくれるので便利ではあるが、
リトライが走った際に key が埋まったままリトライされてしまうので注意した方がいい。


インデックス爆発

https://cloud.google.com/datastore/docs/concepts/indexes#index_limits


Go から Datastore を扱うためのライブラリ

1. gae の datastore package
https://github.com/golang/appengine/tree/master/datastore
datastore の 公式ライブラリ。
goon, nds というラッパーは存在するが、
基本的に datastore package の API に沿っているので、
まずはこれを使って datastore を叩いてみるといいと思う。


2. goon
https://github.com/mjibson/goon
datastore package がよしなにやってくれないところをやってくれるラッパー。

以下を備える。
・entity を Get() した際に memcache へのキャッシュしてくれる。
 ただい、Query + Filter で取得した entity はキャッシュしてくれない。
・ローカルメモリキャッシュ
 memecache だけではなく、メモリにもキャッシュしてくれる。
・easy な API
 既存の simple な API を使いやすくラップしてくれている。
 以下の記事が分かりやすい。
 http://qiita.com/soundTricker/items/194d4067b0e145544b56
・datastore.ErrFieldMismatch の回避 
 http://pospome.hatenablog.com/entry/2017/07/19/213944


ただ、ローカルメモリキャッシュには注意。

こちらも確認しておくといい。
http://pospome.hatenablog.com/entry/2017/05/05/182836


3. nds
https://github.com/qedus/nds
memcache へのキャッシュ機能のみを提供するライブラリ。
キャッシュ機能しか提供しないので、
nds が提供する API は datastore package の API に準拠している。
goon のように easy な API は提供していない。


目を通すべき記事(随時 update していきます)

このブログに載せようと思った内容なのですが、
すでに記事があるので載せておきます。
目を通しておく必要があります。
https://cloud.google.com/datastore/docs/best-practices
https://sinmetal-slide.appspot.com/2016/gaejanight0607/gaejanight0607.slide#1
http://qiita.com/vvakame/items/f98b5189b302cd729734
https://cloud.google.com/datastore/docs/articles/balancing-strong-and-eventual-consistency-with-google-cloud-datastore/
https://cloud.google.com/datastore/docs/articles/fast-and-reliable-ranking-in-datastore/
http://qiita.com/hogedigo/items/25b4dbefe694dbfc7dbc
http://qiita.com/hnw/items/52addaebe95bb9a16eac


TaskQueue にもトランザクションをかけることができる

https://cloud.google.com/appengine/docs/go/datastore/transactions#transactional_task_enqueuing
GAEのTaskQueueにもDatastoreのトランザクションが有効。

Datastoreの更新とTaskQueueへのエンキューを一緒に実行するケースは多いと思うので、
トランザクションをかけた方がいい。
ロールバックしてくれる。

ただし、1つのトランザクションでは最大でも5つしかエンキューできない。

An application cannot insert more than five transactional tasks into task queues duing a single transaction



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

http://pospome.hatenablog.com/entry/2017/03/15/204132


kindが存在しなくても複合インデックスを貼ることができる

kind作ってからインデックス貼らないといけないのかなと思っていたが、kind 作らなくても貼れる。


ローカルのエミュレータの挙動とGCP上での挙動が同じとは限らない

たしか、ローカルのエミュレータSQLite が裏で動いているので、GCP上の Datastore と挙動が同じとは限らない。
基本的にちゃんとエミュレートしてくれるっぽいが、GCP上で実動作を確認した方がいい。


(おまけ)Datastore の特徴

なんとなく Datastore 自体の特徴を書いておこうかなと・・・。


レプリケーションという概念がない
RDBだと Slave遅延を考慮して、マスター、スレーブ を意識してコードを書くケースもあるが、
Datastore だとそういったところを意識せずにコードを書けばいい。

スケールする
RDBレプリケーションモデルだとマスターがスケールしないのが欠点で、
シャーディングで対応することがあるが、
Datastore はレプリケーションがないので、データ量が増えても、リクエスト数が増えてもスケールします。

速くもないが、遅くもない
memcache のような KVS をイメージすると、ハイパフォーマンスなものをイメージするかもしれないが、
Datastore は特別速い KVS ではない。
ただ、高いスケーラビリティを持っているわりには遅くもない。
自分の経験ですが、普段使いで困るほど遅くなることはない印象。

意外と柔軟な検索ができる
memcache のような KVS をイメージすると「key ベースでの get しかできない」と思うかもしれないが、
意外と柔軟な検索ができる。
ただ、RDBほど柔軟にできないので注意した方がいい。

バランスがいいので、逆にハマる
スケールする & 遅くない & そこそこ柔軟な検索ができる ので、
利用者の多くのニーズをカバーできると思う。
反面、できないこともあるので、そこでハマることがある。


まとめ

特にないです。(´・ω・`)

間違っているところがあれば
コメント or twitter で教えてください。

Datastore/GO で datastore: flattening nested structs leads to a slice of slices: field xxx

以下のエラーが出た。

datastore: flattening nested structs leads to a slice of slices: field xxx


Datastore は以下のように struct をネストすることができるが、

type A struct {
    B []B
}

type B struct {
    Name string
}


以下のようにネストした struct (今回で言うとB)が slice を持つと、
このエラーになる。

type A struct {
    B []B
}

type B struct {
    Name string
    Score []int //これがダメ
}

Go で cannot assign to xxx のエラー

ググると色々情報があるが、
日本語の情報がないので書いておく。

https://play.golang.org/p/atdCt2Mken

これを実行すると「cannot assign to struct field users[0].Connected in map」のエラーが出る。

結論から言うと map が問題らしい。

go - Why do I get a "cannot assign" error when setting value to a struct as a value in a map? - Stack Overflow

ここに答えっぽいのが書いてあるけど、
正直良くわからなくて、
map はアドレス指定できないもので、
実行中にそれを指定して参照しようとすると、このエラーが出るっぽい。
https://golang.org/ref/spec#Address_operators
http://golang.jp/go_spec#Address_operators

以下の記事でも map の値が移動するような記述がある
http://wazanova.jp/items/856

新しいbucket配列のメモリが利用可能になると、古いbucket配列のkey/valueペアは新しいbucket配列に移動("evacuated" = 避難)する。key/valueペアが追加もしくは削除されるタイミングで、移動は起きる。古いbucketで同じ場所にあったkey/valueペアは、別々の新しいbucketに移る可能性がある。key/valueペアを均等に配置しようというアルゴリズムが働くからである。


最初に提示した以下の場合は・・・
https://play.golang.org/p/atdCt2Mken

以下のようにするとエラーにならない。
https://play.golang.org/p/8OEZ-hKN9G

map で key 指定した struct ごと変更してあげれば大丈夫っぽい。
map の参照がアドレス指定できない場合はエラーになるようなので、
今回のように対象が struct 以外の場合でも(string などでも)エラーになる。

twitter にてポインタにするとエラーにならないとのメッセージをいただきました。

たしかにエラーにならないですね。
自分は基本ポインタ使うので、遭遇しなかったのかな。
https://play.golang.org/p/3aflz7y1ku


これ詳しく分かる人、コメント or twitter で教えていただけると助かります・・・。

Go のブランク識別子を利用した import による pakcage への副作用

ORMを調べていると以下のような import を見かけた。

import _ "github.com/go-sql-driver/mysql"

調べてみると、
これはブランク識別子を利用した import で、
import 対象のパッケージを初期化するためのものらしい。

つまり、
init() や package value として宣言している変数を初期化するために
わざわざ import していることになる。

ただ、なぜ初期化が必要なのかが分からなかった。

以下で書いたようにわざわざブランク識別子で import しなくても、
そのパッケージに依存するパッケージが import してくれるはずなのでは?
pospome.hatenablog.com


調べてみると、
DBのドライバを差し替えるために
ブランク識別子による import が必要であることがわかった。

golang には database/sql というDBを扱うパッケージがある。
これはSQLの発行やトランザクションの実行などを提供するもので、
DB操作はこのパッケージだけで完結する。

そして、DBごとの差異(接続方法の違いなど)はドライバという形で、
別途用意されている。
https://github.com/golang/go/wiki/SQLDrivers#drivers

なので、利用するDBによって、利用者がドライバを import する必要がある。

import (
	"database/sql"
	_ "github.com/go-sql-driver/mysql" //mysqlのドライバを利用
)


上記の go-sql-driver/mysql を import すると以下が実行され、
database/sql にドライバがセットされる。
https://github.com/go-sql-driver/mysql/blob/master/driver.go#L182

ドライバがセットされないと、以下でエラーになる。
https://github.com/golang/go/blob/master/src/database/sql/sql.go#L572

Oracle, Postgres を利用したい場合はそれ用のドライバを import すればいい。

ORMは database/sql をラップして
クエリビルダやstructへのマッピングなどの便利機能を提供しているので、
ドライバに影響を受けることはない。
ドライバが Oracle だろーが、Postgres だろーが問題なく動作する。

ただ、ドライバが提供するのは SQL を実行するために最低限必要な実装だけなので、
ORM側で各DBの違いを吸収する必要がなくなるわけではない。

例えば、以下のようなクエリビルダを提供する場合・・・・

orm.Find().Table("tb_test").Limit(10)

MySQL であれば以下のSQLになるが・・・

SELECT * FROM tb_test LIMIT 10;

Oracle であれば以下のSQLになる。

SELECT * FROM tb_test WHERE ROWNUM <= 10;

こういったSQLの方言はORM側で吸収する。

xorm でも各DBごとに実装が存在する。
https://github.com/go-xorm/xorm/blob/master/mssql_dialect.go
https://github.com/go-xorm/xorm/blob/master/mysql_dialect.go
https://github.com/go-xorm/xorm/blob/master/oracle_dialect.go