- 筒井悠平
- 約 3,800 文字
- 2,300 View
目次
ライブリッツの筒井です。
GoのORマッパー連載、折り返して5日目です。
SQLBoilerを使用したDBスキーマ駆動なREST APIサーバの開発ワークフローを紹介します。
なぜSQLBoilerを選ぶのか?
自分たちのチームでは、REST APIサーバを開発する際にはまずデータベースのテーブル設計から始めることが多いです。その次にAPI定義の設計へ入るのですが、既にテーブル定義は出来上がっているため、なんとなくSQL文が頭に思い浮かんだ状態でAPIのRequest / Responseを考えることになります。
ゆえにO/Rマッパに一番に求めるのは、「いかにストレスなく思い描いていたSQL文を実行し、Goの文脈に持ち込めるか」ということです。
この基準を元に、次のような観点からSQLBoilerを選定しています。
複雑なSELECT文でDSLに苦悩したくない
前述の通り、我々の頭の中にはなんとなくのSELECT文が既に浮かんでいます。このSELECT文を組み立てるために、O/Rマッパ特有のDSLに悪戦苦闘することは避けたいものです。
SQLBoilerのクエリビルダはSQLの基本的な構文と大きく違わないため、直感的に使用できました。またGoのコードが生成されているため、カラム名、テーブル名を指定する際にコード補完が効くのも嬉しいポイントです。
相関サブクエリを使った集計などはクエリ相当複雑になってしまいますが、Raw SQLの実行、StructへのBindも容易なので、「複雑なクエリはSQLをそのまま実行」というアプローチが取れます。
(これについては jmoiron/sqlx も同様です)
INSERT, UPDATE, DELETEはSQLを書きたくない
SELECT文が複雑になることは多々ありますが、INSERT, UPDATE, DELETEはそうでもありません。
これらのDMLはO/Rマッパに乗っかり、Type Safeに書きたいところです。
SQLBoilerでは、INSERT, UPDATE, DELETEはStructのメソッドとしてコードが生成されます。JSONからUnmarshalしてInsertといった処理が簡単に書けます。
Schema Migrationはいらない
これには「データベースの寿命 > アプリケーションの寿命」という前提があります。
Migration機能も含め、データベースをアプリケーションからは独立した1サービスとして扱う、という考え方をとっています。
一方「データベースの寿命 == アプリケーションの寿命」とできる場合は、ActiveRecordのようにデータベースをアプリケーションの1機能として扱えたほうが開発効率は上がるでしょう。
SQLBoilerにはSchema Migration機能は含まれておらず、既存のデータベースからコードを生成するアプローチを取っています。
別ツール (Flywayを使うことが多いです) でSchemaを管理している自分たちにはピッタリでした。
SQLBoilerの使い方
プロジェクトのセットアップについては公式ドキュメントに詳しいため割愛します。
テーブル定義
今回使用するテーブル定義です。
ユーザ、イベント、イベント参加ユーザの3テーブルで、イベント・ユーザ間は 1 : 0...N の関係です。
create table "user" ( |
シンプルなINSERT, SELECT
まずはユーザ作成処理を考えます。
先にコードを掲載します。
func CreateUser(ctx context.Context, b []byte) (*boiler.User, error) { |
boiler.User は、SQLBoilerによって生成された user テーブルに対応するStructです。
カラムに設定したコメントが反映されているのが地味に便利なポイントです。
// User is an object representing the database table. |
実行結果は次の通りです。
DB側で採番されたIDがStructにセットされています。便利ですね。
func TestCreateUser(t *testing.T) { |
{ |
次にユーザ一覧を取得する処理を考えます。
select * from user where organization_id = $1 |
Goのコードは次の通りです。
var db *sql.DB |
この程度のシンプルなクエリであれば自動生成コードで簡単に実装可能です。
実行結果は次のようになります。json タグもSQLBoilerが付けてくれているので、サクッとSerializeが可能です。
func TestListUsers(t *testing.T) { |
[ |
中間テーブルのJOINを含むSELECT
イベントとその主催者ユーザ、参加者ユーザ一覧を取得する処理を考えます。
SQLで書くならば次のようになるでしょう。
select "event".*, "participant".* |
SQLBoilerで実装する場合、Eager Loadingによって結合先テーブルを読み込む形になります。Eager Loadingのコードも、SQLBoilerが外部キー制約を読み取って自動生成してくれています。
クエリ時に qm.Load() で結合先テーブルを読み込んでおけば、FromTable.R.JoinTable の形式でGoからアクセス可能です。
JoinのJoinも、ドット区切りで結合先テーブルを記述すると読み込み可能です。
type Event struct { |
実行結果は次の通りです。
[ |
実行されたSQLは次の3つでした。Eager Loadingがきちんと効いていますね。
SELECT * FROM event ORDER BY date desc; |
集計を含むSELECT
参加者数の多いイベントを取得する処理を考えます。
SQLは次の通りです。
select event.*, coalesce(r1.participants, 0) as participants |
ここまで来るとクエリビルダに頭を悩ませるのも大変なので、SQLをそのまま実行させます。
type EventPopularity struct { |
実行結果は次の通りです。
[ |
SQLBoilerのつらいところ
Bulk Insertが出来ない
SQLBoilerで生成されたコードにBulk InsertのAPIは含まれていません。複数件のInsertを行うためにはfor文を使う、などの対応となります。
ただ、テンプレートを追加することでBulk Insertのコードを生成している方もいるようです。
参考: https://qiita.com/touyu/items/4b25fbf12804f12778b7
テーブル設計に若干の制約が生じる
主な制約は以下の2つです。
- 関連するテーブルには外部キー制約を設定する必要がある
- 中間テーブルには結合先2テーブルの主キーを使った複合主キーを設定する必要がある
- 上記例の
event_userテーブルでは、複合主キー(event_id, user_id)を設定することが必須です。
- 上記例の
これらの制約は主にEager Loadingのためのもので、クリアできない場合には各テーブルを手動でJoinする必要が出てきます。
プロジェクトによっては「外部キー制約は使わない」という設計ポリシーを取っていることもあるかと思いますので要注意ポイントです。
そもそもデータベースが無いと開発が進まない
これはSQLBoilerが悪いわけではありません。
ただ「データベースを元にコードを生成する」というアプローチ上、
開発初期で頻繁にテーブル設計を変更するようなシーンで煩わしさを覚えてしまうのは致し方ありません。
このようなシーンが頻発するのであれば、Migration機能を持つGORMなどのほうが適任かもしれません。
SQLBoilerとoapi-codegenによるREST APIサーバ開発
タイトル回収です。SQLBoilerとoapi-codegenによるコード生成を活用したREST APIの開発フローを紹介します。
oapi-codegenの詳細はここでは割愛します。以下記事を御覧ください。
c.f. Go の Open API 3.0 のジェネレータ oapi-codegen を試してみた
まず、設計書としてデータベースのDDL、API定義のopenapi.ymlが用意されている前提です。
これを元に、DBアクセスを担うパッケージ boiler と、ルーティング他を担う restapi を生成します。
我々が開発するのは、上記2パッケージのグルーコード、ビジネスロジックを担う app.go です。
開発例
今回は例として次のようなOpenAPI定義を用意しました。
openapi: 3.0.0 |
この定義を元にoapi-codegenを実行すると、次のようなStructが出来ます。
package model |
このStructに、gomodifytags によって boil タグを追加します。openapi.yml の x-oapi-codegen-extra-tags を利用すると任意のタグの付与もできますが、ひとつひとつのフィールドに追加するのは漏れが出そうだったので、gomodifytagsを使っています。
package model |
あとはSQLの実行結果をこのStructにBindすれば完成です。
この例ではAPI定義の User のフィールドはすべてテーブル定義の User に含まれているため、SQLBoilerで生成されたSelect文から直接Bindが可能です。
func ListUser(ctx context.Context) ([]*model.User, error) { |
参考までに、コード生成のためのMakefileを掲載します。
|
まとめ
SQLBoilerの使い方と、これを使ったREST APIサーバの開発フローを紹介させていただきました。
私は過去約3年ほど、数プロジェクトでSQLBoilerを採用しています。
この間Goは1.10から1.16となり、SQLBoilerはv2からv4に(主にGo modules対応で破壊的変更はありませんでした)進化しています。
その性質上うまくハマらないプロジェクトもあるかと思いますが、うまくハマればとても使いやすいライブラリと感じています。
なお、今回利用したコード、プロジェクトはこちらのリポジトリにまとめてあります。
次は伊藤真彦さんのSQLビルダーgoquの使い方です。