Go の ORM / query builder 消耗日記
自分が Go の Web アプリを触り始めてからそろそろ1年になる.会社的にはずっと Ruby on Rails を使っていて,新しいプロダクトをリリースするにあたり一部マイクロサービスで Go を使い始めて1年経った?それくらいの頃.サーバは gin で,ORM は GORM だった.
GORM
GORM は 2018/08 現在で 9.7 k stars を集める超人気 ORM .ちょっと Ruby の activerecord っぽい API を持つクエリビルダを持ち,なぞのちからで Associations (has_many から, polymorphism 的なのまで)や Callback にも対応する.
db.Where(&User{Name: "izumin5210"}).First(&user)
// SELECT * FROM users WHERE name = "izumin5210" LIMIT 1;
// ちなみに,`Where` に struct 突っ込むとだいたい後悔する
Ruby on Rails に慣れた人が多いというチーム事情もあり,近い使い心地の ORM を検討する中で採用されたんだと思う.しらんけど(当時を知らないので適当言ってる).似たようなところでいうと xorm など? golang orm
みたいなワードで検索すると人類が苦しんだあとがたくさん見つかっって便利.
Go に慣れてないのも相まってか,かなり苦しんだ.
- GORM のお気持ちを考えながらコードを書く必要がある.
- GORM 自体の実装が,なんかめっちゃ難しい.なにか問題を踏んだときに,それがバグなのか仕様なのかすら調べられない程度には実装が複雑.
ひとしきり消耗したあと,じつはsqlx みたいな超薄いクエリビルダだけでいいんじゃないかと思った.
sqlx
sqlx は薄い database/sql.DB
のラッパ + ヘルパ関数群である.はじめて使ったのはたぶん ISUCON の練習だった.これ自体はほんとに責務が少ない.
- クエリの結果の struct / slice への bind
- placeholder をいい感じに(
$1
or?
とか,foo IN (?)
と slice とか)
よくあるパターン.
- ActiveRecord パターンに夢を見て消耗した人類が,でもやっぱり struct への bind くらいはしてほしいよなあって思ったときに使うやつ
- 「SQL を手で書いたほうがわかりやすいのでは?!」という真理に到達したときに使うやつ
社内のある gin + GORM なマイクロサービスをフルリライトする機会があって,そのときに採用しようとした.SELECT
とか全部自分でかけて超楽じゃん!って最初はノリノリで使っていた.しかし,ある程度書き進めていくと,INSERT
や UPDATE
で異常に消耗する事に気づく.
// このあとどんどんカラムが増えていく…
db.ExecContext(ctx, "INSERT INTO (user_id, title, body) VALUES (?, ?, ?)", userID, post.Title, post.Body)
これ毎回ほんとに書くの?ってなった.「struct から INSERT
できるようにしない?(意訳)」という Issue ができているが,作者は Modl を使うと良いよって言ってた.あとは gorp とかと併用するみたいな話もよく聞くが,2つ以上のパッケージの知識を要求されるのは悩ましい….
正気に戻ったタイミングで,ずっと目をつけていた SQLBoiler を試すことにした.
SQLBoiler
SQLBoiler を構成するものは2つある.DB のテーブルのスキーマをもとにいい感じの struct 定義を生成する CLI と,その生成物を中心とした ORM + クエリビルダである.クエリビルダに関しては ActiveRecord-like を標榜するパッケージに多い builder pattern — メソッドチェインでクエリを組み立てる ものではなく,functional option pattern で実現されており,Go に入って Go に従った感があって好感が持てる.
一方で,SQLBoiler が謳う ActiveRecord-like productivity の本質はおそらくコード生成によるものである.下記のコードは,生成される struct 定義の一部である(実際は下記に加えクエリビルダのエントリポイントになる関数群も生成される).
struct の定義は結構いろいろなことを考慮してくれている.
- Foreign Key Constraints を見て,Rails で言うところの
has_many
やbelongs_to
といった関連を生成してくれる - NOT NULL Constraints を見て,nullable なカラムはちゃんと
null
パッケージをつかってくれる
など.
この,「実際のテーブル定義をもとに struct 及び関数定義が生成される」というのが非常に体験が良い.NOT NULL や Foreign Key をつけ忘れた瞬間にコード書く手間が激増することになるので,硬いテーブル定義を作らざるを得なくなるという,きょうせいギプス的な効果が得られて手に入る努力値も2倍になるしすばやさは下がらない(むしろ上がる).
ライブラリとしての筋はとても良いので,もうちょっと使い込んでみようと思っている.ちなみに,いま(2018/8時点)で v3 がリリース目前なので,今使うなら RC 版をちゃんと指定するといいです.
ORM に何を求めるか
ここまでの消耗を振り返って,自分が ORM に求めるものはなんなのか考えてみた.だいたい以下のようなものが欲しいらしい.
INSERT
/UPDATE
/DELETE
クエリのビルド- placeholder まわりの helper(
?
と$
の使い分け,数をいい感じに,etc.) - クエリ結果 → struct / slice of struct へのマッピング
- 関連レコードの Preloading & マッピング
日常生活で困らない程度には書けるので,SELECT
のクエリビルディングに関しては実はあまり興味がない.それで「 ORM / query builder いらないのでは?!」って一瞬勘違いしそうになった.けど,INSERT
や UPDATE
は長くてダルいしカラム増えたり変わったりしたときの対応が大変.そんなことに人生を浪費したくないので,パッケージ側でよしなにカバーしてもらいたい.
ORM とコード生成
Ruby の activerecord は,DB のテーブル定義を読んだ上でランタイムにクラス定義を拡張している.Android の ORM である Orma や Room はアノテーションでテーブル定義を宣言し,それをもとにクラス定義やクエリを呼ぶメソッドを生成する.Ruby はちょっと Go から遠すぎるから一旦おいておくとしても,Java で比較的快適な ORM がコード生成により実現されているのなら,Go でも ORM はコード生成によるアプローチを考えるのが正道かなとずっと考えていた.そういうのもあり SQLBoiler に目をつけていて,いまのところ大きく期待からは外れてなさそうだと思っている.
おわりに
自分がこれまでどのように消耗したかを振り返って,そこから「ほんとは何がほしいのか」「どういうアプローチがいいのか」を考えてみた.まだもうちょっとだけ消耗の日々は続きそうな気がする.
自分は DB に詳しいわけでもないふつうの Web アプリケーションエンジニアなので,なんかバイアスかかってるかもしれない.
本稿は timakin さんの以下の記事に触発されて書かれました.