sqlc と golang-migrate で PostgreSQL を扱う
これは Go Advent Calendar 2021 の 5 日目の記事です。
はじめに
sqlc は SQL から Go のコードを生成するライブラリです。一般的な ORM にはコードから SQL を生成するものが多いと思いますが、 sqlc はそれらと真逆のアプローチを取るユニークなライブラリです。
本記事では sqlc と golang-migrate を使って PostgreSQL を扱う Go アプリケーションの構築を行ってみます。
- kyleconroy/sqlc: Generate type safe Go from SQL
- golang-migrate/migrate: Database migrations. CLI and Golang library.
ディレクトリ構成
.
├── db
│ ├── db.go
│ ├── migrations
│ │ ├── 000001_create_users_table.down.sql
│ │ └── 000001_create_users_table.up.sql
│ ├── models.go
│ ├── queries
│ │ └── users.sql
│ └── users.sql.go
├── generate.go
└── sqlc.yaml
db/migrations/*
マイグレーションファイルを配置します。
これらのファイルを使って後ほど golang-migrate でマイグレーションを行います。
CREATE TABLE IF NOT EXISTS users (
id serial PRIMARY KEY,
username VARCHAR (50) UNIQUE NOT NULL,
password VARCHAR (50) NOT NULL,
email VARCHAR (300) UNIQUE NOT NULL
);
DROP TABLE IF EXISTS users;
db/queries/*
クエリファイルを配置します。
それぞれのクエリにはコメントで sqlc のためのアノテーションを記述します。
-- name: GetUser :one
SELECT * FROM users WHERE id = $1;
-- name: ListUsers :many
SELECT * FROM users ORDER BY id;
-- name: CountUsers :one
SELECT count(*) FROM users;
-- name: CreateUser :exec
INSERT INTO users (username, password, email) VALUES ($1, $2, $3);
-- name: UpdateUser :exec
UPDATE users SET username = $2, password = $3, email = $4 WHERE id = $1;
-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;
INSERT
や UPDATE
は RETURNING
句をつけてクエリアノテーションに :one
を指定することで作成・更新後の行を戻り値で取得できるようになります。
-- name: CreateUser :one
INSERT INTO users (username, password, email) VALUES ($1, $2, $3) RETURNING *;
-- name: UpdateUser :one
UPDATE users SET username = $2, password = $3, email = $4 WHERE id = $1 RETURNING *;
sqlc.yaml
sqlc による生成を行うために設定ファイルを用意します。
version: "1"
packages:
- name: "db" # 生成されるパッケージ名
path: "db" # 生成されるディレクトリ
queries: "./db/queries/" # クエリファイルのあるディレクトリ
schema: "./db/migrations/" # マイグレーションファイルのあるディレクトリ
engine: "postgresql"
generate.go
sqlc によるコード生成は sqlc generate
で行うことができます。
go generate
経由で生成を行うようにするため、下記のディレクティブを埋め込んでおきます。
//go:generate go install github.com/kyleconroy/sqlc/cmd/sqlc@latest
//go:generate sqlc generate
あとは go generate
を行うだけでいいので、インストールなどを気にする必要がありません。
$ go generate .
db/*
sqlc generate
を行うと 3 種類のファイルが生成されます。
db/db.go
db/models.go
db/*.sql.go
db/db.go
トランザクションのインタフェースやクエリメソッドを定義する構造体が宣言されます。
// Code generated by sqlc. DO NOT EDIT.
package db
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}
db/models.go
テーブルに対応する構造体が宣言されます。
// Code generated by sqlc. DO NOT EDIT.
package db
import ()
type User struct {
ID int32
Username string
Password string
Email string
}
また、 sqlc は ALTER TABLE
にも対応しています。
ALTER TABLE users ADD COLUMN address varchar(300) NOT NULL;
上記のようにカラムを追加して再生成を行うと、構造体にも対応するフィールドが追加されます。
type User struct {
ID int32
Username string
Password string
Email string
+ Address string
}
db/*.sql.go
クエリ毎にメソッドが定義されます。
func (q *Queries) CountUsers(ctx context.Context) (int64, error)
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) error
func (q *Queries) DeleteUser(ctx context.Context, id int32) error
func (q *Queries) GetUser(ctx context.Context, id int32) (User, error)
func (q *Queries) ListUsers(ctx context.Context) ([]User, error)
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error
アプリケーションコード
ここまでで準備ができたので、実際にアプリケーションのコードを書いていきます。
sqlc と golang-migrate にはそれぞれいくつかの使用方法が用意されていますが、今回はひとつの DB インスタンスをマイグレーションとクエリで使い回せるようにするため、 *sql.DB
を使う方法で進めます。
*sql.DB
の取得
sql.Open()
で *sql.DB
を取得します。ドライバには pgx を使用します。
import _ "github.com/jackc/pgx/v4/stdlib"
db, err := sql.Open("pgx", dataSourceName)
golang-migrate を使ってマイグレーションを行う
取得した *sql.DB
を使ってマイグレーションを行います。
import (
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
// 1. *sql.DB を渡してドライバを作成
driver, err := postgres.WithInstance(sqldb, &postgres.Config{}) // sqldb = *sql.DB
// 2. 作成したドライバを元にマイグレーションインスタンスを取得
m, err := migrate.NewWithDatabaseInstance(
"file://db/migrations", // マイグレーションファイルのあるディレクトリ
"postgres",
driver,
)
// 3. マイグレーションを適用
err := m.Up()
このようにマイグレーションをアプリケーションに埋め込む場合、実際にはマイグレーションが多重実行されないような配慮が必要でしょう。
sqlc が生成したコードを使ってクエリを発行する
sqlc が生成した db
パッケージのコンストラクタで *Queries
のインスタンスを取得します。
取得したインスタンスにはすべてのクエリメソッドが定義されているので、クエリ発行が必要な処理で使い回すことができます。
// *sql.DB を渡して *Queries のインスタンスを取得
queries = db.New(sqldb) // sqldb = *sql.DB
// *Queries のメソッドを使ってクエリを発行
users, err := queries.ListUsers(ctx)
おわりに
複雑な SQL が含まれるアプリケーションでは、 ORM の様々な機能を駆使してそれらを実現するよりも、 sqlc を使う方が楽な場面もありそうだと感じました。今後フィットするプロジェクトがあれば使用を検討してみたいと思います。
Discussion
もしかしたらですが、000001_create_users_table.down.sql と 000001_create_users_table.up.sql の内容は逆ではないでしょうか?
逆でしたね。修正しました。
ありがとうございます。