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

タオルケット体操

サツバツいんたーねっと

Go言語でのORMを色々検討してみた

フルスタックと聞いたからrevel使ってみたのにモデル層のサポートどころかORM(あるいはそれを良い感じに使うレール)もないじゃねえかみたいな感じで仕方なくちょろっと調査した、みたいな所感だけを書いた雑なエントリです。

結論

色々やろうとすると最終的には諦めてdatabase/sqlを使うのが良い。割り切りが大事。

試したライブラリ

  • GORM https://github.com/jinzhu/gorm

    多分一番リッチ。
    後で書くけど、リレーションを含めたコードファーストなデータストア定義とかをしたいならこれしか選択肢なさそう。

  • gorp https://github.com/go-gorp/gorp

    正確には"ORM-ish library for Go"だけどね

  • genmai https://github.com/naoina/genmai

    "Simple, better and easy-to-use ORM library for Golang"
    kochaを作った人、日本人作。こちらもORMというよりはクエリビルダーって感じである。

GORM

リレーションを含めたリッチな(というかActiveRecord的な)ORMはこれしかないのでは。

他にもマイグレーションとかも整備されてるし、APIもいかしてる。

ただちょろっと見てみて、マジカルすぎてGo言語文化との相性どうなのとか、パフォーマンスチューニングとかエラー時の調査大変なんじゃねとか考えてビビって萎えてしまいやめた。
こういうのを使いたいマインドだとGo言語での開発厳しいんじゃないかみたいな気持ちです。参考にならなくてすまん。

gorp

構造体からのテーブル生成は最低限の機能のみ対応している。
ただ本当に最低限なので、全てのカラムがPascalCaseになってしまうし、MaxSizeだとかその他諸々の属性はタグじゃなくてコードで書く必要があってめんどい。

InsertHookだとかの機能は備えている。

そもそもクエリビルダでもないので、SQLをいっぱい書かされる。db.Select(obj, "SELECT * FROMとか書かされるのつらい。
構造体からの簡単なテーブル生成と、sql.Rows.Scan()への簡易ラッパーだと割り切って使うくらいの気持ちでいる方が精神衛生上良い。

Go言語のsql.Rows.ScanのAPIはかなり原始的で辛いので、そこが簡単になるのは嬉しい。
でも正直あんま使う意味を見いだせなかった。

genmai

構造体からのテーブル生成はgorpよりも多機能で、リレーション以外の定義は大体出来る。リレーションと外部キーの設定を出来るようにするパッチを作ることも検討したんだけど、クエリ生成と構造体マッピングの部分とかの兼ね合いで頭がフットーしそうになったので断念した。

ただ、構造体埋め込みの検知に対応してたり、フィールドを自動的にsnake_caseにしてテーブル生成してくれたりとかゆいところに手が届く。
やってることの中身もプリミティブなクエリビルダなので、何が行われているのかが明瞭で問題が起きたときのチューニングもしやすそう。

トランザクションまわりのAPIが微妙に使いにくい気もするけど、まぁなんとかなる。

選ばれたのは玄米でした

これ以上調査してもあんま大差なさそうなのでこんくらいで切り上げた。
GORM以外はどれもあんま変わらねえとかシンプルさとか色々なあれで、genmaiが一番いいなという結論に達した。

玄米食で毎日健康!

運用

そもそもが、RubyとかPythonみたいに黒魔術でゴリゴリやってく言語じゃないのでORMに機能を求めまくるのは良くないのではとおもった。

コードファーストでテーブル定義からマイグレーションをやろうとすると、GORMみたいにマッチョになるか、他のminimalなライブラリみたいに妥協を重ねたものになる。
そもそもGo言語のオブジェクト、というか構造体の機能や型システムがなんとなくSQLと相性良くないように感じて色々とつらい。これなら正規化とかしない方がいい。

なのでGo言語でRDBを使う場合は、テーブルのスキーマ定義、マイグレーション用のスクリプトやDSLを別に用意して良い感じに自動化タスクを作ってやるのがベターなんじゃねみたいなことを検討している。なんでもかんでもGo言語内でやる必要はないのでは。

genmaiに関しても、簡単なクエリに関しては備え付けのクエリビルダを使うけど、複雑なクエリは容赦なくSQL手書きするのが結局一番シンプルだ。シンタックスチェックが効かなくなるけど、もうそういうのは諦めよう。テストすればいいじゃん。

ややこしいクエリを発行するとき

genmaiは(他のも出来るとおもう)db.DB()でdatabase/sql.DBを取り出せる。
だから諦めてdatabase/sqlを使おう。Stmtを使えばSQLインジェクションの心配も(多分)ないし、冷静に考えるとこれで必要充分だよもう。

以下は参考的なコード

type User struct {
    Id       int64  `db:"pk"`
    Name     string `db:"unique" size:"30"`
    Age      int64
}

type Comment struct {
    Id       int64  `db:"pk"`
    Text     string
    UserId   int64
    UserName string `db:"-"`
}

func main() {
    db := genmai.New(&genmai.SQLite3Dialect{}, "./app.db")
    stmt, _ := db.DB().Prepare(`
      SELECT
          "comment".*,
          "user".name as UserName
      FROM "commend" LEFT JOIN "user" on "commend".user_id = "user".id
      WHERE "commend".user_id = ?
  `)
    rows, _ := stmt.Query(1)
    var comments []Comment
    for rows.Next() {
        var comment Comment
        rows.Scan(
            &comment.Id,
            &comment.Text,
            &comment.UserId,
            &comment.Username,
        )
        comments = append(comments, comment)
    }
    fmt.PrintLn(comments)
}

もちろんgenmaiにもJOINをビルドするAPIはあるんだけど、ビルドされるフィールドの指定が暗黙的に"テーブル名".*みたいになるため、JOINで持ってきたテーブルのカラムを参照するようなことが出来ない。

アプリケーションの規模が大きくなればなるほど、database/sqlの方が良さそうな気がしてきた。

なお僕はDB設計よくわからないので、もしかしたらリレーションとか外部キー制約とかJOINとか必要ないものなのかもしれない。なんかチューニングまわりでそういう話もきく、リレーションしないRDBってよくわかんないけど。

まとめ

ActiveRecordがやりたければGROMか、いっそのことRuby使うしかない。

個人的にGo言語でテーブルのコードファーストは辛い気がする。

スキーマやマイグレーションの管理は別のツールと組み合わせてMakeとかRakeでタスクにまとめつつCIすればよくねという感。

どちらにせよ標準ライブラリのdatabase/sqlの使い方は覚えておく必要がある。
あとSQLそのものも知っておく必要がある(これはどんなORM使おうが必須だけど)。

みたいな感じですね。

Go言語、気持ちよくなってきた。


あと全然関係ないんですけど、RevelとORマッパーを組み合わせて使う系のチュートリアル(Booking | Revel - A Web Application Framework for Go! とか)ってどれもこれもControllerを拡張していく書き方を推奨してるんですけど、ControllerがドメインどころかDBの管理までするのってどうなの? なんかおかしくないですか?
Revel、全然フルスタックじゃないし「チュートリアルをやってみた」みたいなのばっかであんま使ってる話も聞かないしnet/httpとかGojiを使うべきだったのかもしれない。つらい。