🐘

sqlc と golang-migrate で PostgreSQL を扱う

2021/12/05に公開
2

これは Go Advent Calendar 2021 の 5 日目の記事です。

はじめに

sqlc は SQL から Go のコードを生成するライブラリです。一般的な ORM にはコードから SQL を生成するものが多いと思いますが、 sqlc はそれらと真逆のアプローチを取るユニークなライブラリです。

本記事では sqlc と golang-migrate を使って PostgreSQL を扱う Go アプリケーションの構築を行ってみます。

ディレクトリ構成

.
├── 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 でマイグレーションを行います。

000001_create_users_table.up.sql
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
);
000001_create_users_table.down.sql
DROP TABLE IF EXISTS users;

db/queries/*

クエリファイルを配置します。
それぞれのクエリにはコメントで sqlc のためのアノテーションを記述します。

user.sql
-- 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;

INSERTUPDATERETURNING 句をつけてクエリアノテーションに :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 にも対応しています。

000002_alter_users_table.up.sql
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

クエリ毎にメソッドが定義されます。

users.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

pankonapankona

もしかしたらですが、000001_create_users_table.down.sql と 000001_create_users_table.up.sql の内容は逆ではないでしょうか?

  • 記事中では down のほうに CREATE するクエリが書かれており、up のほうに DROP するクエリが書かれているようです。
  • up のほうに CREATE する類のクエリを書くのが自然かなと思いました。
tchssktchssk

逆でしたね。修正しました。
ありがとうございます。