🕌

プロダクト開発でsqlcを採用した話

2024/08/15に公開

はじめに

toB向けの0->1のGoのバックエンドAPIの開発でsqlcを採用しました。

使い始めてから1年半くらい経ったので感想を書いてみようと思います。他の人のブログでよく言及されている点については同じことを書くことになるので書きません。

使っていたsqlcのバージョンは1.18~1.26です。

sqlcを採用した理由

sqlcに限らずバックエンドAPIの開発の技術選定をする上で技術的な要件は無かったです。開発効率や開発速度を高めることができる技術選定を求められていました。

バックエンドAPIの開発のリードエンジニアは私だったので、私が使い慣れているツールをなるべく使い、技術検証や使い方を調べる時間を極力減らし開発効率と開発速度を上げようとしていました。ただ、全て私が知っているツールだと私の開発のモチベーションが上がらなかったので、Product Ownerに相談してORMのみ使ったことが無いツールを採用することにしました。

GoのORMを調査していたときに時雨堂のVoluntasさんのブログやフューチャー社のブログを読んでsqlcを知りました。gormを使っているときにORMで生成されるSQLクエリの内容が一見わかりづらいことに悩んでいたので、SQLクエリを中心に開発ができるsqlcに興味を持ち、独断と偏見と好奇心で採用しました。うろ覚えですが以下のブログを読んだ気がします。

https://voluntas.medium.com/2022-年に学んで良かった技術-321848e1b09c

https://future-architect.github.io/articles/20221128a/

使ってみて良かったところ

SQLクエリを中心に開発ができる

SQLクエリでDBにレコードをCRUDする処理を書くためにsqlcを採用したと言ってもいいです。実際に使ってみた感想は、難しいところもありましたが大変良かったです。

特に良いと思ったのは、やはりSQLクエリでDBにレコードをCRUDする処理を書くことができる開発体験の良さです。SQLクエリの書き方さえわかっていれば、開発者はSQLクエリを書いてGoのコードを生成し、生成されたメソッドを使うだけでいいです。

例えば以下のように書けます。

-- name: ListUsers :many
SELECT * FROM users;
// sqlcで生成されたGoのコード
func (q *Queries) ListUsers(ctx context.Context) ([]User, error) {
    ...
}

新しく開発者が入ったときに最初からsqlcについて詳細に説明する期間を設ける必要はありませんでした。例えばgormのようなORMを使う場合、ある程度gormの使い方に慣れてもらう期間を設けます。XXXのSQLクエリを生成しようと思ったらgormのXXXメソッドではなくYYYメソッドを使ってください、みたいなコードレビューやペアプロをすることがあります。sqlcの場合は複雑なSQLクエリを除いてsqlcの使い方を教えることはありませんでした。
(gormにはgormの良さがあります。)

複雑なSQLクエリを書いたときに、DBに接続してSQLクエリを実行して期待した処理が実行できることを確認し、動作確認したSQLクエリからGoのコードを直接生成できるところも開発体験が良かったです。

sqlcを採用したタイミングでsqlcの開発が活発になった

sqlcを使い始めてから少し経ったころ、sqlcのMaintainerが会社を設立しsqlcの開発にフルコミットすることになりました。私がOSSの技術選定をするときは継続的に開発されていることや利用者が多いことを重視しているので、重視している条件の状況がより良くなりそうで安心しました。

https://github.com/sqlc-dev/sqlc/discussions/2411

使ってみて難しかったところ

出力されるメソッドの命名規則

sqlcでは、あらかじめ定義したSQLクエリからSQLクエリを実行するGoのコードを生成します。

私が開発していたバックエンドAPIでは、sqlcで生成されたメソッドを組み合わせて使ってdomain objectのDBへのCRUDを実装していました。実装の都合でsqlcで生成したメソッドを使い回すようにしていたのでメソッド名はSQLクエリの内容がわかるように命名するルールにしていました。

「SQLクエリを中心に開発ができる」の例のように条件が少ないSQLクエリだと問題ないですが、条件が多くなったりJOIN等があると正確に命名しづらくなります。

例えば以下のような例です。

-- name: CountNotArchivedUsersOfNotArchivedOrganization... :many
...

開発している途中で複雑なSQLクエリが増え、「メソッド名からSQLクエリの内容がわかるようにする」ルールが崩壊しかけました。特にSQLクエリが複雑になったのがクエリ用のエンドポイントでビュー都合のSQLクエリを発行する場合です。崩壊しかけたときはチームルールでクエリの内容を要約する単語を用意し、メソッドを命名するときに要約した単語を利用することでなんとか耐えたと思います。

解決策の案としてアプリケーション内部でビュー依存のクエリを分離することを検討しました。経験的にビュー依存のSQLクエリは使い回すことが少ないので、ビュー依存のクエリ用のメソッドをユースケースに対応する名前で命名しようと考えていました。

以下のような実装イメージです。複数のSQLクエリを実行するのであれば処理する順番でsuffixに番号をつければいいかなと思っています。

-- name: QueryForXXX1 :many
...

-- name: QueryForXXX2 :many
...

上記の案を実際には採用しませんでした。一部のクエリのみ分離するとチームルールがわかりづらくなるのでビュー依存のクエリを全て分離することを検討しましたが、ビュー依存のクエリを全て分離する工数と他タスクの優先度を考慮して対応することを諦めました。あらかじめ考慮できていれば最初からビュー依存のクエリを分離して開発していたと思います。

SQLクエリの条件を使い回すテンプレートの仕組みが無い

sqlcの設計思想と異なる可能性が高いことや、私がSQLクエリからコードを生成するツールに詳しくない前提で読んでください。

sqlcにSQLクエリの条件を使い回すテンプレートの仕組みが無いので、毎回SQLクエリの全文を書いていました。gormのようにメソッドチェーンでクエリの条件を追加できる場合は条件を付与する関数を用意して使い回すことができます。

自前でテンプレートの仕組みを作るとしたら、SQLクエリを書くときは専用のプレースホルダーをセットしておいて後でプレースホルダーを置換する仕組みを用意するとかかなと思ってました。今回の開発では頑張ればなんとかなったので毎回SQLクエリの全文を書いていました。

テンプレートを使う以外の方法だと、DBのViewを利用したり、Read専用のテーブルを用意するなどDB側の工夫でSQLクエリの複雑さを減らす方法があります。今回は0->1のプロダクト開発で今後のビジネスの方針の不確実性が高かったため、開発側で大きく方針を変更せず、DBに手を入れずにアプリケーション側でSQLクエリを頑張って書くことにしました。

複雑なSQLクエリを書いたときのsqlcのバージョンアップ

sqlcを1.18から1.20にバージョンアップするときに一部の複雑なSQLクエリから生成されたGoのコードが正常に動作しませんでした。v1.20で組み込み関数のsqlc.Sliceに破壊的変更が加わり、sqlc.Sliceを使う複雑なSQLクエリのみ一部の動作が変わったためです。当時はチームメンバーにsqlcのコード生成のロジックを深く理解してもらってSQLクエリを修正しました。

ただ、1年半開発していてこのような経験は1回のみでした。

私がsqlcを採用するか検討するときに考えること

まず、複雑なSQLクエリを書く必要があるかについて考えます。

複雑なSQLクエリを書くような状況の例として、フロントエンドから利用されるバックエンドAPIの開発があります。フロントエンドから利用される場合、ビュー依存でデータを取得するので複雑なSQLクエリを発行する可能性が高いと個人的に思います。

具体的な例でいうとフィルタリングがあります。フィルタリングの条件に応じて動的にSQLクエリの条件が付いたり消えたりする場合、sqlcでは事前にSQLクエリを書く必要があるので、あらかじめSQLクエリの条件の分岐に対応するSQLクエリを書くか複雑なSQLクエリを書いて対応する必要があります。

複雑なSQLクエリを書く可能性が高い場合はsqlcを採用するかどうかを深く検討します。

次に、sqlcで出力されるメソッドの命名規則を決め、必要に応じてアプリケーションコードのアーキテクチャも決めることができるかを確認します。「出力されるメソッドの命名規則」の例で言えば、アプリケーションコード内でコマンドとクエリのコードを分離しておき、クエリで利用するsqlcで生成されるコードのメソッド名は簡略化するなどを検討します。後からアーキテクチャを変更すると大変なのでsqlcの導入と同時にアーキテクチャも変更します。

上記の検討で問題が無ければsqlcを使いたいと思います。

おわりに

色々と書きましたが、sqlcを採用したときの開発体験はとても良かったです。

SQLの書き方さえ知っていればORMの仕組みを知ることなくほぼ使えるのは素晴らしいと思いました。動的に様々なデータの引き方をされるようなユースケースが無ければ迷うことなく採用できると思います。例えば、私が普段開発している認証認可のバックエンドAPIだと複雑なSQLクエリを書く頻度が少ないので採用しやすいです。

sqlcを使った事例はまだ多くないと思うので、開発するアプリケーションのユースケースに合っていればぜひ採用を検討してみてください。

Discussion