sqlcでSQLからGoのコードを生成する

kyleconroy/sqlcSQLからGoのコードを生成してくれるツールです。
schemaやSELECT文などからコードを生成してくれます。

今回は基本的な使い方を紹介します。

インストール

$ go install github.com/kyleconroy/sqlc/cmd/sqlc@latest

brewでもインストール可能です。

使い方

設定ファイル、schema、queryの準備

sqlcを使うために3つのファイルを準備します。

.
├── query.sql
├── schema.sql
└── sqlc.yml
# sqlc.yml

version: 1
packages:
  - path: "db"
    name: "db" # package name
    engine: "postgresql"
    schema: "schema.sql"
    queries: "query.sql"
-- schema.sql

CREATE TABLE users (
  id uuid NOT NULL PRIMARY KEY,
  name text NOT NULL
);

Get, Create, Update, Delete, List, CountのSQLを書きました。
これらのSQLからコードを生成することになります。

-- query.sql

-- name: Get :one
SELECT * FROM users
WHERE id = $1 LIMIT 1;

-- name: Create :one
INSERT INTO users (
  id, name
) VALUES (
  $1, $2
)
RETURNING *;

-- name: Update :exec
UPDATE users SET name = $2
WHERE id = $1;

-- name: Delete :exec
DELETE FROM users WHERE id = $1;

-- name: List :many
SELECT * FROM users
ORDER BY id;

-- name: Count :one
SELECT count(*) FROM users;

コードを生成する

$ sqlc generate -f sqlc.yml

db下にファイルが生成されました。

.
├── db (生成されたファイル)
│   ├── db.go
│   ├── models.go
│   └── query.sql.go
├── query.sql
├── schema.sql
└── sqlc.yml

models.go

// Code generated by sqlc. DO NOT EDIT.

package db

import (
    "github.com/google/uuid"
)

type User struct {
    ID   uuid.UUID
    Name string
}

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,
    }
}

query.sql.go

// Code generated by sqlc. DO NOT EDIT.
// source: query.sql

package db

import (
    "context"

    "github.com/google/uuid"
)

const count = `-- name: Count :one
SELECT count(*) FROM users
`

func (q *Queries) Count(ctx context.Context) (int64, error) {
    row := q.db.QueryRowContext(ctx, count)
    var count int64
    err := row.Scan(&count)
    return count, err
}

const create = `-- name: Create :one
INSERT INTO users (
  id, name
) VALUES (
  $1, $2
)
RETURNING id, name
`

type CreateParams struct {
    ID   uuid.UUID
    Name string
}

func (q *Queries) Create(ctx context.Context, arg CreateParams) (User, error) {
    row := q.db.QueryRowContext(ctx, create, arg.ID, arg.Name)
    var i User
    err := row.Scan(&i.ID, &i.Name)
    return i, err
}

const delete = `-- name: Delete :exec
DELETE FROM users WHERE id = $1
`

func (q *Queries) Delete(ctx context.Context, id uuid.UUID) error {
    _, err := q.db.ExecContext(ctx, delete, id)
    return err
}

const get = `-- name: Get :one
SELECT id, name FROM users
WHERE id = $1 LIMIT 1
`

func (q *Queries) Get(ctx context.Context, id uuid.UUID) (User, error) {
    row := q.db.QueryRowContext(ctx, get, id)
    var i User
    err := row.Scan(&i.ID, &i.Name)
    return i, err
}

const list = `-- name: List :many
SELECT id, name FROM users
ORDER BY id
`

func (q *Queries) List(ctx context.Context) ([]User, error) {
    rows, err := q.db.QueryContext(ctx, list)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    var items []User
    for rows.Next() {
        var i User
        if err := rows.Scan(&i.ID, &i.Name); err != nil {
            return nil, err
        }
        items = append(items, i)
    }
    if err := rows.Close(); err != nil {
        return nil, err
    }
    if err := rows.Err(); err != nil {
        return nil, err
    }
    return items, nil
}

const update = `-- name: Update :exec
UPDATE users SET name = $2
WHERE id = $1
`

type UpdateParams struct {
    ID   uuid.UUID
    Name string
}

func (q *Queries) Update(ctx context.Context, arg UpdateParams) error {
    _, err := q.db.ExecContext(ctx, update, arg.ID, arg.Name)
    return err
}

生成されたコードを使う

.
├── app.go
├── db
│   ├── db.go
│   ├── models.go
│   └── query.sql.go
├── query.sql
├── schema.sql
└── sqlc.yml

app.goに生成されたコードを使う部分を書いていくとこんな感じになります(このままでは動かないですが、雰囲気は伝わるかと思います)
WithTx() を使うことでトランザクションも使用可能です。

// app.go
func createUser(ctx context.Context) error {
    postgresDB, err := sql.Open("postgres", "")
    if err != nil {
        return err
    }

    queries := db.New(postgresDB)

    tx, err := postgresDB.BeginTx(ctx, nil)
    if err != nil {
        return err
    }
    args := db.CreateParams{
        ID:   "00000000-0000-0000-0000-000000000000",
        Name: "inari111",
    }
    if _, err := queries.WithTx(tx).Create(ctx, args); err != nil {
        return err
    }

    if err := tx.Commit(); err != nil {
        return err
    }

    return nil
}

使ってみてどうだったか

  • SQLからGoのコードを生成できるのは便利だと思った
  • NOT NULL制約をつけなければ sql.NullString でモデルの生成された
  • sqlc compile で構文や型のエラーをチェックすることも可能
  • k0kubun/sqldef で冪等にschema管理管理をし、sqlcでコードを生成する。といった使い方ができそうだと思った
  • こういった処理はどれも似たようなコードになるので、自動生成することで開発時間の短縮につながりそう