sqlcを使ってデータ作成と取得を行う方法

Go の ORM ツールの一つである sqlc を使用する機会があったので、sqlc を使って、簡単なデータ取得のコードを記述するところまでを書き留めていきます。

# バージョン
Mac で下記バージョンにて動作確認を行なっています。

| 対象       | バージョン |
| ---------- | ---------- |
| PostgreSQL | 14         |
| Go         | 1.21.1     |
| sqlc       | 1.23.0     |

# sqlc とは

sqlc とは Go の ORM の一つです。
ORM とは、Object-Relational Mapping(オブジェクト関係マッピング)のことで、オブジェクトと RDB(Relational Database)のマッピングを行うものとなります。
ORMを使用することで、SQL を直接書くことなく、オブジェクトのメソッドで DB を操作することができます。

# sqlc の初期設定

データベース操作を行う事前準備として、　sqlcの初期設定を行います。

## 1. sqlc.yaml を作成する

下記コマンドを実行し、sqlc.yaml を作成します。

```cmd
$ sqlc init
```

## 2. sqlc.yaml を記述します

```yaml:sqlc.yaml
version: "1"
packages:
  - name: "db"
    path: "./db/sqlc/"
    queries: "./db/query/"
    schema: "./db/migrations/"
    engine: "postgresql"
    emit_json_tags: true
    emit_exact_table_names: false
    emit_empty_slices: true
```

| 項目                   | 説明                                                                                                       |
| ---------------------- | ---------------------------------------------------------------------------------------------------------- |
| version                | バージョン                                                                                                 |
| name                   | 自動生成されるコードのパッケージ名を設定                                                                   |
| path                   | 自動生成されるコードの格納先のディレクトリを設定                                                           |
| queries                | 実行する SQL 文をの格納先のディレクトリを設定                                                              |
| schema                 | マイグレーションファイルの格納先のディレクトリを設定                                                       |
| engine                 | 使用する DB 名を設定。`mysql`または`postgresql`                                                            |
| emit_json_tags         | 自動生成されるコード内のモデル定義の構造体に json マッピング用のタグを付与するか設定                       |
| emit_exact_table_names | DB のテーブル名を単数形に直した名称をモデル定義の構造体の名前とするかを設定 |
| emit_empty_slices      | 複数レコードを取得するクエリを実行したとき、取得数が 0 件の場合、true: 空のスライス、false: nil を返却する |

# プリペアードステートメントを作成・queries に指定した格納先に設置

## プリペアードステートメントとは

SQL 文を動的に生成したいとき、変更する箇所を変数のようにした SQL 文を作成し、値の挿入をプログラムで行う仕組みのことをいいます。

## プリペアードステートメント例

users テーブルからデータを取得するプリペアードステートメントを作成します。
users テーブルに対しユーザー作成(CreateUser)をするプリペアードステートメントと、users テーブルからメールアドレスでユーザーを取得する(GetUserByEmail)プリペアードステートメントを作成します。

```sql:users.sql
-- name: CreateUser :one
INSERT INTO users (
    user_name, email, phone_number, password
) VALUES (
    $1, $2, $3, $4
) RETURNING *;

-- name: GetUserByEmail :one
SELECT
    *
FROM
    users
WHERE
    email = $1;
```

※`RETURNING *`を用いて、ユーザー作成時、登録した内容を返すようにします

# プリペアードステートメントに基づき、Go コード生成

下記コマンドを実行すると、`./db/sqlc`フォルダに Go コード作成されます。

```cmd
$ sqlc generate
```

## 生成されたコード

```golang:db.go
// Code generated by sqlc. DO NOT EDIT.
// versions:
//   sqlc v1.23.0

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

```golang:models.go
// Code generated by sqlc. DO NOT EDIT.
// versions:
//   sqlc v1.23.0

package db

import (
	"database/sql"
)

type User struct {
	ID          int64          `json:"id"`
	UserName    string         `json:"user_name"`
	Email       string         `json:"email"`
	PhoneNumber string         `json:"phone_number"`
	Password    sql.NullString `json:"password"`
}
```

```golang:user.sql.go
// Code generated by sqlc. DO NOT EDIT.
// versions:
//   sqlc v1.23.0
// source: user.sql

package db

import (
	"context"
	"database/sql"
)

const createUser = `-- name: CreateUser :one
INSERT INTO users (
    user_name, email, phone_number, password
) VALUES (
    $1, $2, $3, $4
) RETURNING id, user_name, email, phone_number, password
`

type CreateUserParams struct {
	UserName    string         `json:"user_name"`
	Email       string         `json:"email"`
	PhoneNumber string         `json:"phone_number"`
	Password    sql.NullString `json:"password"`
}

func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
	row := q.db.QueryRowContext(ctx, createUser,
		arg.UserName,
		arg.Email,
		arg.PhoneNumber,
		arg.Password,
	)
	var i User
	err := row.Scan(
		&i.ID,
		&i.UserName,
		&i.Email,
		&i.PhoneNumber,
		&i.Password,
	)
	return i, err
}

const getUserByEmail = `-- name: GetUserByEmail :one
SELECT
    id, user_name, email, phone_number, password
FROM
    users
WHERE
    email = $1
`

func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) {
	row := q.db.QueryRowContext(ctx, getUserByEmail, email)
	var i User
	err := row.Scan(
		&i.ID,
		&i.UserName,
		&i.Email,
		&i.PhoneNumber,
		&i.Password,
	)
	return i, err
}

```

# データ作成と取得

生成されたコードを用いて、データの作成と、取得を行います。

```golang
package main

import (
	"context"
	"database/sql"
	"fmt"
	db "golang-qiita/db/sqlc"
	"log"

	_ "github.com/lib/pq"
)

func main() {

	conn, err := sql.Open("postgres", "postgresql://postgres:postgres@localhost:5432/qiita?sslmode=disable")
	if err != nil {
		log.Fatal("DBに接続できません。", err)
	}

	arg := db.CreateUserParams{
		UserName:    "test",
		Email:       "test@example.com",
		PhoneNumber: "123456789",
		Password:    sql.NullString{String: "password", Valid: true},
	}

	queries := db.New(conn)
	insertResult, err := queries.CreateUser(context.Background(), arg)
	if err != nil {
		log.Fatal("登録に失敗しました。", err)
	}

	fmt.Println(insertResult)

	selectResult, err := queries.GetUserByEmail(context.Background(), "test@example.com")
	if err != nil {
		log.Fatal("取得に失敗しました。", err)
	}

	fmt.Println(selectResult)
}
```

```cmd
$ go run main.go
{1 test test@example.com 123456789 {password true}} // insertした内容
{1 test test@example.com 123456789 {password true}} // selectした内容
```

# おわりに

sqlc を使用すると簡単に DB 操作のメソッドが作成できました！
今回は簡単な操作でしたが、他にも少し変則的な操作もやってみたので、今後まとめていきたいと思います。

# 参考

- [sqlc Documentation](https://docs.sqlc.dev/en/latest/)
- [【シンプルかつ即戦力！】Go 言語 / sqlc の使い方！](https://qiita.com/SDTakeuchi/items/292ae0c01be945ef47a0)
- [sqlc と dockertest でデータベースを使ったテストを書こう](https://gihyo.jp/article/2023/12/tukinami-go-09)