1クール続けるブログ

とりあえず1クール続けるソフトウェアエンジニアの備忘録

sqlcを利用してGoのDB周りの処理を扱う

記事一覧はこちら

背景・モチベーション

以前にsqlcの作者のブログ記事を読んだことがきっかけです。だいぶ前におそらくredditで話題になっていたものを拾った気がします。
さっと見て開発生産性が上がりそうだなと思ったので試してみました。

sqlcとは

github.com

SQLからtype-safeなコードを生成してくれるツールです。
長い間、ソフトウェアエンジニアはプログラミング言語のアノテートされたオブジェクトからSQLのクエリを生成してきていました。SQLは既に構造化・型付けされた言語のため、SQL自体から正しいtype-safeなコードを生成すべきというのが、作者のモチベーションのようです。
現在のところはPostgreSQLMySQLのみをサポートしており、SQLiteも今後はサポート予定のようです。

sqlcを利用する方法

  1. SQLを書きます
  2. sqlcを実行し、1.で作成したクエリに対するtype-safeなinterfaceを提供するGoのコードを生成します
  3. 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句は未サポート
    • #364 によると、「CASE句で対応可能では」という提案がされていますが、Postgresは5回以上のクエリで実行計画をキャッシュすることになるがCASE句を利用した場合にはキャッシュが利用できないのでパフォーマンスが悪化する
    • MySQLではCASE文の中にあるパラメータを認識してくれない
  • [MySQL] LIMITとOFFSETで名前付きパラメータを書けない
  • [MySQL] sqlc.arg(foo) IS NULLのパラメータを無視する
  • [MySQL] 同名の名前付きパラメータを別々に認識する

sqlcを実際に動かしてみる

サンプルのリポジトリは下記です。isucon10のisuumoの実装を参考にしながらsqlcを取り入れてみました。
(謎のスイッチが入ってしまってDDDライクに作ってしまった…)

github.com

sqlcのバージョンはv1.8.0です。
まず、sqlcを実行するディレクトリ配下に置く設定ファイルの生成を行います。
queriesschemaで指定したディレクトリパスには、それぞれアプリケーション内で実行する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.DBsql.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はとてもイケてるなと思うツールの反面、まだまだ枯れておらず実践で利用するのには難しい印象があります。
データベースを扱うのってやっぱり難しいなという平易な感想で締めさせていただきます、以上!