sqlcを利用してGoのDB周りの処理を扱う
記事一覧はこちら
背景・モチベーション
以前にsqlcの作者のブログ記事を読んだことがきっかけです。だいぶ前におそらくredditで話題になっていたものを拾った気がします。
さっと見て開発生産性が上がりそうだなと思ったので試してみました。
sqlcとは
SQLからtype-safeなコードを生成してくれるツールです。
長い間、ソフトウェアエンジニアはプログラミング言語のアノテートされたオブジェクトからSQLのクエリを生成してきていました。SQLは既に構造化・型付けされた言語のため、SQL自体から正しいtype-safeなコードを生成すべきというのが、作者のモチベーションのようです。
現在のところはPostgreSQLとMySQLのみをサポートしており、SQLiteも今後はサポート予定のようです。
sqlcを利用する方法
- SQLを書きます
sqlc
を実行し、1.で作成したクエリに対するtype-safeなinterfaceを提供するGoのコードを生成します- sqlcが生成したメソッドをユーザのコードから呼びます
sqlcのメリット
コード生成をしている過程で、全てのクエリとDDL文を解析します。テーブルの列の名前や型が一致しない場合には、sqlcはクエリのコンパイルに失敗することで実行時エラーを未然に防ぐことができます。
sqlcが生成するメソッドは厳密なGoの型定義を持つため、クエリの引数やカラムの型を変更した時にコードを更新しなければコンパイルに失敗するようになっています。
安全ですね…!
database/sql
パッケージを直接利用することのデメリット
クエリが増えてくると、マッピングをメンテナンスするのが面倒になり生産性に大きな影響を与えます。
更に良くない点として、下記の3点のような実行時まで気づけないミスを犯してしまう可能性があります。
- クエリのパラメータの順番を入れ替えた時に、マッピングの更新が漏れる
- テーブルにカラムが追加された時に、追加したカラムの値を返却するようにクエリを変更する作業に漏れが生じる
- SQLの方とコードの型に相違がある
gormなどの高レベルライブラリを利用することのデメリット
先程言及したdatabase/sql
パッケージを直接利用することのデメリットは解消されますが、クエリテキストやstructタグを使用して手動でのマッピングをせざるを得ず、間違った場合の実行時エラーが起こる可能性は大きくは減っていない状況と言えます。
sqlcを実際使ってみての感想
この後に動かしてみたことについて記載しますが、感想を先に述べておこうと思います。
当初からサポートしているPostgresはともかく、MySQLはまだまだ実用には至れない状況だと思います。
確認できた限りで下記のような未サポート/不具合があります(Issueに上がってなさそうなものは自分の方でIssue上げてみました)。
- [Postgres] bulk impot(
COPY
句)は未サポート - [Postgres/MySQL] 動的where句/order by句は未サポート
- [MySQL] LIMITとOFFSETで名前付きパラメータを書けない
- [MySQL]
sqlc.arg(foo) IS NULL
のパラメータを無視する - [MySQL] 同名の名前付きパラメータを別々に認識する
sqlcを実際に動かしてみる
サンプルのリポジトリは下記です。isucon10のisuumoの実装を参考にしながらsqlcを取り入れてみました。
(謎のスイッチが入ってしまってDDDライクに作ってしまった…)
sqlcのバージョンはv1.8.0
です。
まず、sqlcを実行するディレクトリ配下に置く設定ファイルの生成を行います。
queries
とschema
で指定したディレクトリパスには、それぞれアプリケーション内で実行するSQLとテーブルを定義するSQLを格納しておきます。
このpath
パラメータの配下にファイルが生成されます。
$ cat <<EOF > sqlc.yaml version: "1" packages: - name: "persistence" path: "pkg/persistence" queries: "_sql/queries" schema: "_sql/schema" engine: "mysql" emit_prepared_queries: false EOF
それでは、コードを生成していきます。
先程作成したsqlc.yaml
が存在するディレクトリで、sqlc generate
を実行することでコードを生成することができます。syntaxにミスがある場合には、このときにエラーという形で確認できます。
${SQL_FILENAME}.sql.go
: アプリケーションのコードから呼び出すデータアクセスメソッド郡db.go
:sql.DB
やsql.Tx
のためのinterfaceを用意models.go
: テーブル定義をGoのstructとして表現したもの
$ sqlc generate $ ls -l pkg/persistence total 32 -rw-r--r-- 1 44smkn staff 4.5K 5 23 18:06 chair.sql.go -rw-r--r-- 1 44smkn staff 567B 5 23 18:06 db.go -rw-r--r-- 1 44smkn staff 415B 5 23 18:06 models.go
定義したSQLと生成されたコードの一例としては下記のようになります。このGetChair()
メソッドをアプリケーションから呼び出してデータベースから情報を取得します。
拙いコードですが、上述のリポジトリでレコードのInsertや検索もやってみていますので、良かったら覗いてみてください。
CREATE TABLE chair ( id BIGINT NOT NULL AUTO_INCREMENT, name TEXT NOT NULL, description TEXT NOT NULL, thumbnail TEXT NOT NULL, price INT , height INT , width INT , depth INT , color TEXT , features TEXT , kind TEXT , popularity INT NOT NULL, stock INT NOT NULL, PRIMARY KEY (id) ); -- name: GetChair :one SELECT * FROM chair WHERE id = ?;
const getChair = `-- name: GetChair :one SELECT id, name, description, thumbnail, price, height, width, depth, color, features, kind, popularity, stock FROM chair WHERE id = ? ` func (q *Queries) GetChair(ctx context.Context, id int64) (Chair, error) { row := q.db.QueryRowContext(ctx, getChair, id) var i Chair err := row.Scan( &i.ID, &i.Name, &i.Description, &i.Thumbnail, &i.Price, &i.Height, &i.Width, &i.Depth, &i.Color, &i.Features, &i.Kind, &i.Popularity, &i.Stock, ) return i, err }
sqlc
はとてもイケてるなと思うツールの反面、まだまだ枯れておらず実践で利用するのには難しい印象があります。
データベースを扱うのってやっぱり難しいなという平易な感想で締めさせていただきます、以上!