GoのDBライブラリと俺たち、それからsqlla

年末ですね。カヤックでは360度評価の時期でもあるので、みんな振り返りだとか内省などの言葉がいたるところで飛んでいます。この記事でも今年の出来事を振り返りしてみたいと思います。どうも、ソーシャルゲーム事業部ゲーム技研の谷脇です。

この記事はTech KAYAC Advent Calendar 2019 Migration Trackの20日目の記事です。19日目はAWS Lambda Node.js runtime の EoL に疲れたので Go にしていっている話でした。

この記事のあらまし

  • あるWebサービスを作るプロジェクトでORMを切り替えた
    • 開発言語はGo言語
  • DBライブラリ/ORMはgithub.com/xo/xoを使っていました
  • ですが開発途中から、私が作成したライブラリであるgithub.com/mackee/go-sqllaに乗り換えました
  • どっちもコード生成系だけれど、アプローチがぜんぜん違う
  • sqllaとか誰も聞いたことがないと思うので、使い方も解説する

GoのDBライブラリ/ORM事情(社内)

RubyにおけるRuby on Rails/Active RecordのようなデファクトスタンダードのフレームワークやORMがないGoでは、そういったライブラリ選びというのを手探りでやっているのが実情です。

そして、カヤックで作っているWebサービスの永続化データストアにはRDBMSが多く用いられており、GoでWebサービスを作るときにどうやってSQLを組み立てて発行し、どうやって結果を利用するかというのは、模索が続けられていました。

あるときは、gorpやGORMと言った比較的世間で多く使われているORMに類するライブラリを使い、あるプロジェクトではよく使うクエリや、structへ結果をマッピングするコードを、DBスキーマを見て自動生成するなどの手法が取られていました。

今回の話は、とあるGoのプロジェクトで使っていたxoを、途中からsqllaに切り替えたので、この2つのライブラリの比較をしてみようと思います。

そもそもORMでしたいことってなんだっけ

巷のWebサービスをRDBMSを使って開発する際には、よくORMと呼ばれるライブラリが使われます。カヤックではRubyを使って作るときにはActive Recordを、Perlを使って作るときにはDBIx::Classが使われています。

このORMたちは、アプリケーション内でどういった目的で使われているのでしょうか?

これは私の意見ですが、一言で言うとRDBMSのクエリ言語・データ形式とプログラミング言語のパラダイムとのインピーダンスミスマッチの解消であると思います。

クエリ言語、つまりSQLのことを指していますが、1つのプログラミング言語の中に別の言語が混ざると好ましくない事が起こります。例えば、SQLを文字列で表現する場合に、ミスタイプを検知できない場合があったり、静的型付け言語を使っているのに、SQLでは静的型であることの恩恵が受けられないケースがあったりします。

また、SQLを手書きし、配列などの形で結果を受け取る場合、それをプログラミング言語側の構造体やオブジェクトにマッピングするコードを記述しなければなりません。このようなマッピングするコードは、ほとんどが似ていて、少しだけ違うと言った感じになります。静的型付け言語においてはこの手間が顕著に現れます。

多くのORMの関心はこの2つの問題の解消であり、2つの機能に分離することが出来ます。

  • クエリビルダー
    • プログラミング言語側に寄せた形でのクエリ言語の組み立てを行う機能
  • オブジェクトへのマッピング
    • まさにObject Relational Mappingの部分
    • Goの場合は結果をstructへマッピングする機能
    • structを書き換えてDB側に反映させたいときに、UPDATE文を発行する機能も付け足される
    • DB上の行にメソッドを生やし、行を中心に処理するようなパターンにも用いられる

この2つは独立して提供されるケースもあります。ちなみにsqllaはクエリビルダーに、structへのマッピング機能を後から追加したようなライブラリです。

また、ORMにはDBとの接続管理を行ったり、トランザクションの管理を行うものもあります。さらに運用の際に発生する、スキーマのマイグレーションであったり、フィクスチャのロードまでまるっと管理するなど、DBの面倒はまるっと見るものまで、責務の範囲は広げようと思えばいくらでも広げられる分野であると言えます。

こういったライブラリを使わずにアプリケーションを作ることも出来ますが、同じようなコードを何度も書かなければならなかったり、作法が統一されないなどの弊害が起きます。

しかし、多くの機能を提供するようなライブラリだと、アプリケーションに対してORMをあわせる作業よりも、ORMに合わせてアプリケーションを作るような状態になってしまって開発スピードが落ちてしまうようなこともあります。

GoのORMはORMではない?

さて、GoのORMはsimpleを良しとする文化のせいか、ライブラリもミニマムなものが多いように思えます。関連するかのように、この目的に使われるライブラリは、ドキュメント中でORMではないと表明しているライブラリが多く存在します。

gorp

I hesitate to call gorp an ORM.

ORMと呼ぶのは躊躇する。というのは含みがあって、やっていることは他の言語で言うORMに似たことだけれど、そもそもGoにはORMのOがないし、ということは述べられています。Mに対応する部分はちゃんとあって、私が上で述べたようなことを解決するよと書かれています。

squirrel

Squirrel is not an ORM.

squirrelは純粋なクエリビルダーです。

xo

xo is NOT designed to be an ORM or to generate an ORM.

xoはORMとしてデザインされていなく、ORM生成器であると述べています。名付けるならメタORMでしょうか。

READMEにはxoが作られた歴史的背景まで記載されています。読み物として面白いですね。

sqlboiler

It is a "database-first" ORM as opposed to "code-first" (like gorm/gorp).

ORMを名乗るのをためらっているgorpですが、ORMと名乗っているGORMと合わせてこれらはcode-firstなORMであり、sqliboilerはdatabase-firstなORMと主張しているのが面白いですね。

sqlboilerはxoやsqllaと同様に、コード生成をするアプローチですが、思想は全く違うようです。Why another ORMという項にはxoと同じように歴史的背景が存在し、彼らはRailsのActive Recordから移行したようです。その経緯もあってか、xoに比べると多くの機能を提供しています。

sqlla

SQL Builder + ORM-like methods

sqllaはあくまでクエリビルダーが出発点であり、付加的にstructへのマッピング機能を実装しています。

xo の使い方と特徴

xoについてはいろんな方がブログ記事を書いていますし、ドキュメントも詳しいものがREADMEに書かれていますので、そちらを見ていただければと思います。

xoがどんなライブラリか、一言で言うと、DBスキーマとテンプレートをもとにコードを生成するライブラリです。自分でテンプレートを特に書かなくても、大体の用途には適用できるコードが生成され、そのまま使えます。また、テンプレートを自分で書いてさらに使い込むことも出来ます。

今回のプロジェクトではxoを採用していましたが、当初はテンプレートをできるだけ書かずに利用する方針でした。これが間違いだったのですが、プロジェクトの都合に合わせてクエリを自由自在に発行したいとなったときにテンプレートを書かざるをえなくなって、「本当にxoがこのプロジェクトに合っているのか?」検討し、別のライブラリに乗り換えることになりました。

振り返ると、はじめからテンプレートを書いて使い倒すつもりであれば、xoはいい選択肢であったと思います。

sqllaの使い方と特徴

sqllaは私が2015年から作り始めたクエリビルダーです。ドキュメントも少なくスター数も多くないのですが、カヤックでは内製のOTAツールalphawingのDB部分に使っており、実はすでに実績があります。alphawingが出てくる記事。ちなみに公開しているalphawingはver1で、sqllaはver2からの採用です。

たぶん、カヤックの中でも私しか使ってないので、ググっても私が書いたものしか出てきません!

sqllaがどんなライブラリか、一言で言うと、SQLをGoの記法で型安全に書けるライブラリです。サンプルのテストからコードを抜き出すと、

// SELECT * FROM user WHERE name = "hoge" ORDER BY id ASC LIMIT 100
q := NewUserSQL().Select().Name("hoge").OrderByID(sqlla.Asc).Limit(uint64(100))
query, args, err := q.ToSql()

というように、ほぼほぼ書き味としてはSQLと同じですが、Goの文法としてvalidなものであり、またプレースホルダに渡す値も、ちゃんとDBスキーマの型を見て静的に型チェックがされるようになっています。

また、ORM-likeなメソッドもあり、どういうことかというと、

// INSERT INTO user (name) VALUES("hogehoge");
insertedRow, err := NewUserSQL().Insert().ValueName("hogehoge").Exec(db)
// SELECT * FROM user WHERE id = ? [insertedRow.Id]
singleRow, err := NewUserSQL().Select().ID(insertedRow.Id).Single(db)

ここでExecSingleに渡しているdb*database/sql.DBです。内部では、クエリを組み立てた上で*database/sql.DBExecや、QueryRowを行って、さらにstructにマッピングして返しています。

structにはSelectUpdateと言ったメソッドも生えており、ここから自分自身をSELECTしたり、UPDATEするようなクエリをすぐに組み立てられるようになっています。

sqllaはxoと違い、テンプレートを書かずともSQLで表現できる範囲のある程度のクエリを発行できます。逆にJOINなどには(まだ)対応していないので、そういうのはsqlxでやるという割り切りでやっています。

もうひとつデメリットとしてはsqlla独特のクエリの書き方を覚えないといけないことでしょう。SQLと1対1で対応しているので慣れれば簡単ですが、人によっては好き嫌いがあるかもしれません。

sqllaの始め方

sqllaはDBスキーマをstructで表現していることを想定しています。具体的には以下のような感じです。

//go:generate sqlla

//+table: user
type User struct {
  ID        uint64         `db:"id,primarykey"`
  Name      string         `db:"name"`
  Age       sql.NullInt64  `db:"age"`
  Rate      float64        `db:"rate"`
  CreatedAt time.Time      `db:"created_at"`
  UpdatedAt mysql.NullTime `db:"updated_at"`
}

このstructはsqlite3で言うと以下のスキーマに対応します。sqlla自体はMySQLとSQLite3(bulk insertなどを除く)に対応しています。

CREATE TABLE "user" (
    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "name" TEXT NOT NULL,
    "age" INTEGER NULL,
    "rate" REAL NOT NULL DEFAULT 0,
    "created_at" DATETIME NOT NULL,
    "updated_at" DATETIME NULL,
);

もしすでにあるMySQLのスキーマから、structを生成したい場合は、sqllaのリポジトリ内にあるmysql2schemaをご利用ください。今回のxoからの移行でもこのコマンドを使用しました。

structを定義したら、sqllaコマンドを導入した上で、そのソースコードに//go:generate sqllaと加えてgo generateを実行すると、<table名>.gen.goが生成されます。

生成をしたパッケージをexample.com/xxx/xxx/internal/schema、生成元のテーブルをuserとすると、

package hoge

import (
  "database/sql"

  "example.com/xxx/xxx/internal/schema"
)

var db *sql.DB

func fuga() {
  u, err := schema.NewUserSQL.Select().ID(...).Single(db)
  if err != nil {
    ...
  }
  // do something
}

というように使えるようになります。

他に、sqllaで生成したコードで、どういったメソッドや記法が使えるかはサンプルのテストを見てください。

まとめ

テンプレートを書かずに柔軟にクエリを発行し、マッピングするという用途にはsqllaはぴったりでした。

また静的型付けの恩恵を受けて、DBのカラム名の記憶が曖昧でもLSPで保管が行われたりしますし、違う型の値を渡せばコンパイル時に怒ってくれるので、慣れれば非常に便利だと思います。

ただ、私以外が使い始める時にドキュメントの無さや、ユーザの少なさで他のORMに劣ります。ドキュメントはこの記事を含めて充実させたいなと思いますが、ユーザに関してはこれを読んで増えてくれ・・・!と思う次第であります。

要望などあれば日本語でもいいので、GitHubにissueとして上げていただければ可能な限り対応させていただきます。