今回はGo言語におけるORMツールの一つである sqlc を紹介します!
以前までGo言語のORMといえば、私の中では GORM 一択という印象だったのですが、
たまたま触る機会があり、sqlcを使ってみるとGORMとはまた違ったメリットや使いやすいと感じた点もあったので、今回の記事でまとめていきたいと思います。
sqlcの概要
前述の通り、Go言語で使えるORM(ORマッパー)の一つがsqlcです。
GORMはRubyのActive Recordに近い感覚で使えるのに対して、sqlcはSQLで書かれたクエリを自分で用意したうえで、それを元にGo言語のクエリ用のコードを自動生成してくれます。
実行するSQLを事前に書かないといけないという手間は発生しますが、ORマッパーならではの「裏でどんなクエリが飛んでるか分からない」といったことが発生しないために、パフォーマンスの調整がしやすいのが嬉しいところですね。
一方で、現時点でGORMに比べて劣っている点として下記の2点が挙げられます。
- DBのマイグレーションはスコープ外のため、golang-migrateなどの別パッケージを使うなどして別途対応する必要がある。
- 安定したサポートが提供されているのが執筆時点でPostgreSQLのみとなっており、MySQLはベータ的な段階とのこと。
とはいえ、DBマイグレーション用のツールを別途導入したうえで、DBをPostgreSQLに固定するのであれば、
GORMにも引けを取らないような魅力的なツールだと感じているので是非とも一度試し見ていただくのをお勧めしたいです!
なお、コードのサンプルをコチラのレポジトリに用意していますので、コードをじっくり眺めたい方は参考にしてください。
インストール方法
インストールはシンプルです。
GCCが既にインストールされている環境であれば、Macであればbrew install sqlc
を、
もしくは、Go1.17以上がインストールされているのであれば、go install github.com/kyleconroy/sqlc/cmd/sqlc@latest
を実行すればsqlcコマンドが使用可能になるはずです。
インストール後にsqlc version
を実行してバージョンが確認できれば準備完了です。
設定ファイルを作成
続いてはコード生成にあたって必要な設定を書き込むファイルを用意します。
version: "1"
packages:
- name: "db"
path: "./sqlc/"
queries: "./query/"
schema: "./migration/"
engine: "postgresql"
emit_json_tags: true
emit_exact_table_names: false
emit_empty_slices: true
まず必須の設定項目としては下記のものがあります。
version
バージョンを記載します。現在はバージョン2まで存在しますが、今回の例ではバージョン1で進めていきます。
name
自動生成されるコードのパッケージ名を設定
path
自動生成されるコードの設置先のディレクトリ名を指定
queries
実行するSQL文のプリペアードステートメントを設置しておくディレクトリ名を指定
schema
マイグレーションファイルを設置しておくディレクトリ名を指定
engine
使用するDB名を指定("mysql" か "postgresql")
また、任意の設定項目として多数設定可能なものが用意されています。一例としては下記が存在します。
emit_json_tags
default: false
true時に、生成されるコード内のモデル定義の構造体にjsonマッピング用のタグを付与する
emit_exact_table_names
default: true
false時に、DBのテーブル名を単数形に直した名称をモデル定義の構造体の名前として設定する
emit_empty_slices
default: false
複数レコードを取得するクエリを実行して取得数が0件の場合に空のスライスを返却する、falseの時はnilを返却
なお、今回の例ではyaml形式で作成していますが、
json形式もサポートされているので、好みに合わせて使ってもらえばいいかと思います。
マイグレーション用のSQLを作成・設置
次に、上記の設定ファイル内のpackages.schemaに設定したディレクトリにマイグレーション用のファイルを設置します。
なお、現在sqlcがサポートしているgoのマイグレーションツールは下記の5つとなっています。
- dbmate
- golang-migrate
- goose
- sql-migrate
- tern
これらのツールのフォーマットに沿ったマイグレーションファイルであれば、ロールバック用のスクリプトは無視した上で、テーブルの追加やカラムの増減などの既存テーブルへの変更もしっかり対応してコードの生成を行ってくれます。
ただのCREATE文なのであまり参考になっていない気もしますが、例としてコードを乗っけておきます。
CREATE TABLE todos (
id serial PRIMARY KEY,
title VARCHAR(250) NOT NULL,
memo VARCHAR(250),
is_done boolean DEFAULT FALSE NOT NULL,
due_date TIMESTAMP NOT NULL
);
参考:https://docs.sqlc.dev/en/stable/howto/ddl.html
プリペアードステートメントを作成・設置
-- name: GetTodo :one
SELECT * FROM todos
WHERE id = $1 LIMIT 1;
-- name: ListTodos :many
SELECT * FROM todos
ORDER BY due_date;
-- name: CreateTodo :one
INSERT INTO todos (
title,
memo,
is_done,
due_date
) VALUES (
$1, $2, $3, $4
) RETURNING *;
-- name: UpdateTodo :one
UPDATE todos
SET
title = COALESCE(sqlc.narg(title), title),
memo = COALESCE(sqlc.narg(memo), memo),
is_done = COALESCE(sqlc.narg(is_done), is_done),
due_date = COALESCE(sqlc.narg(due_date), due_date)
WHERE
id = sqlc.arg(id)
RETURNING *;
-- name: DeleteTodo :exec
DELETE FROM todos
WHERE id = @deleting_todo_id;
プリペアードステートメント作成にあたっての記法などなど
メタ情報の付与
まずは、それぞれのクエリに対して下記のメタ情報を記載する必要があります。
-- name: GetTodo :one
nameに対しては、自分が分かるようにクエリの処理を表す任意の値を書いてもらえばOKです。
その隣には、クエリ実行後に返却されたいレコードの総数によって、one(返却レコード数が1件)
, many(返却レコード数が不確定)
, exec(返却レコード数が0件)
のいずれかを記載してください。
クエリパラメータ
クエリのパラメータの挿入は下記の3通りの内のいずれかでできます。
$1, $2...$n
@param_name
sqlc.arg(param_name), sqlc.narg(param_name) ※nargはNullを許容する場合に使う
単純なクエリなどの基本的な場合は1の記法で十分ですが、
クエリが複雑になってパラメータに任意の名前を使いたい場合などは2か3の記法を使うといいでしょう。
ただし、上記の中で唯一sqlc.narg(param_name)
のみNullの入力が許容されているため、
動的に生成されるパラメータの内にNullが存在し得る場合は、3の記法で統一する必要があります。
注意点としては、一つのクエリの中で、1から3の記法を混ぜて使用することはできないので、例えば1つのSQL文の中でsqlc.narg()
を使う場合には、同一SQLの中の他のパラメータには$1, $2
は使わずにsqlc.arg()
を使うようにしてください。
コード自動生成の実行
sqlcの設定のファイルも作成して、SQL文の用意もできれば、あとはコマンドでsqlc generate
を実行しましょう。
コマンドを実行すると、設定ファイルのpackages.pathに設定したディレクトリに下記の3種類のファイルが生成されます。
- db.go
DBへのアクセスを行う構造体のコンストラクタがまとめられている - models.go
モデル定義がまとめられている - todo.sql.go
モデルごとに、前出のSQLをベースにしたクエリ関数が書かれる
sql.Open()
の関数から生成された*sql.DB
のインスタンスを引数にdb.go
内のNew()
関数で*Queries
のインスタンスを用意すれば、*Queries.GetTodo(todoId)
の形でレコードの取得が行えるようになります。
また、生成されたコードを見ると分かりますが、先ほどのsqlc.narg()
を使う場合には、下記の例のようにNullStringなどに変わっていて、sqlc.arg()
で指定したIDのみint32となっていることが分かります。
type UpdateTodoParams struct {
Title sql.NullString `db:"title" json:"title"`
Memo sql.NullString `db:"memo" json:"memo"`
IsDone sql.NullBool `db:"is_done" json:"is_done"`
DueDate sql.NullTime `db:"due_date" json:"due_date"`
ID int32 `db:"id" json:"id"`
}
そのため、sqlc.narg()
を使用するパラメータに対応する引数に対しては、string, boolといった値の代わりに、sql.NullString, sql.NullBool等の値を渡す必要があります。
例としてsql.NullStringはこのような定義となっているので、
クエリの関数に値を渡すときには、StringにSQLのパラメータとして挿入したい値を、Validにtrueを割り当てるようにしてください。
type NullString struct {
String string
Valid bool // Valid is true if String is not NULL
}
まとめ
今回の記事ではGo言語で使うことが出来るsqlcというクエリ用のツールの使い方の解説を紹介しました。
マイグレーションまで面倒を見てくれないのは痛いところかもしれませんが、
SQLを書くことに抵抗が無くて、パフォーマンスのコントロールを簡単にできるようにしたい、という人にはうってつけのツールがsqlcかと自分は感じました。
もしもsqlcの使い方を本格的に学びたいという方は、こちらの動画シリーズにおいて本格的なレクチャーも行われているので、ぜひとも参考にしてください。