0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

sqlc: SQLからGoコードを自動生成

Posted at

Group152.png

Leapcell: The Next-Gen Serverless Platform for Golang Hosting

はじめに

Go言語のdatabase/sql標準ライブラリが提供するインターフェイスは比較的低レベルです。これにより、私たちは大量の繰り返しコードを書く必要があります。この多くの定型コードは、書くのが面倒ばかりでなく、エラーが発生しやすいものです。時には、フィールドの型を変更すると、たくさんの場所で修正が必要になる場合があります。新しいフィールドを追加する場合も、以前にselect *クエリステートメントを使用していた場所を修正する必要があります。何か漏れがあると、実行時にパニックが発生する可能性があります。ORMライブラリを使用したとしても、これらの問題を完全に解決することはできません!そこでsqlcが登場です!sqlcは、私たちが書くSQLステートメントに基づいて、型安全で慣習的なGoのインターフェイスコードを生成することができ、私たちはただこれらのメソッドを呼び出すだけです。

クイックスタート

インストール

まず、sqlcをインストールします:

$ go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest

もちろん、対応するデータベースドライバも必要です:

$ go get github.com/lib/pq
$ go get github.com/go-sql-driver/mysql

SQLステートメントの書き方

テーブル作成ステートメントを書きます。schema.sqlファイルに次の内容を書きます:

CREATE TABLE users (
  id   BIGSERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  bio  TEXT
);

クエリステートメントを書きます。query.sqlファイルに次の内容を書きます:

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

-- name: ListUsers :many
SELECT * FROM users
ORDER BY name;

-- name: CreateUser :exec
INSERT INTO users (
  name, bio
) VALUES (
  $1, $2
)
RETURNING *;

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

sqlcはPostgreSQLをサポートしています。sqlcは小さな設定ファイルsqlc.yamlだけを必要とします:

version: "1"
packages:
  - name: "db"
    path: "./db"
    queries: "./query.sql"
    schema: "./schema.sql"

設定の説明

  • version:バージョン。
  • packages
    • name:生成されるパッケージ名。
    • path:生成されるファイルのパス。
    • queries:クエリSQLファイル。
    • schema:テーブル作成SQLファイル。

Goコードの生成

次のコマンドを実行して、対応するGoコードを生成します:

sqlc generate

sqlcは同じディレクトリにデータベース操作コードを生成します。ディレクトリ構造は次のとおりです:

db
├── db.go
├── models.go
└── query.sql.go

sqlcはschema.sqlquery.sqlに基づいてモデルオブジェクトの構造を生成します:

// models.go
type User struct {
  ID   int64
  Name string
  Bio  sql.NullString
}

そして操作インターフェイス:

// query.sql.go
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
func (q *Queries) DeleteUser(ctx context.Context, id int64) error
func (q *Queries) GetUser(ctx context.Context, id int64) (User, error)
func (q *Queries) ListUsers(ctx context.Context) ([]User, error)

このうち、Queriesはsqlcによってカプセル化された構造体です。

使用例

package main

import (
  "database/sql"
  "fmt"
  "log"

  _ "github.com/lib/pq"
  "golang.org/x/net/context"

  "github.com/leapcell/examples/sqlc"
)

func main() {
  pq, err := sql.Open("postgres", "dbname=sqlc sslmode=disable")
  if err != nil {
    log.Fatal(err)
  }

  queries := db.New(pq)

  users, err := queries.ListUsers(context.Background())
  if err != nil {
    log.Fatal("ListUsers error:", err)
  }
  fmt.Println(users)

  insertedUser, err := queries.CreateUser(context.Background(), db.CreateUserParams{
    Name: "Rob Pike",
    Bio:  sql.NullString{String: "Co-author of The Go Programming Language", Valid: true},
  })
  if err != nil {
    log.Fatal("CreateUser error:", err)
  }
  fmt.Println(insertedUser)

  fetchedUser, err := queries.GetUser(context.Background(), insertedUser.ID)
  if err != nil {
    log.Fatal("GetUser error:", err)
  }
  fmt.Println(fetchedUser)

  err = queries.DeleteUser(context.Background(), insertedUser.ID)
  if err != nil {
    log.Fatal("DeleteUser error:", err)
  }
}

生成されたコードはdbパッケージの下にあります(packages.nameオプションで指定されます)。まず、db.New()を呼び出し、sql.Open()の返り値sql.DBをパラメータとして渡してQueriesオブジェクトを取得します。私たちがusersテーブルに対するすべての操作は、このオブジェクトのメソッドを通じて完了する必要があります。

PostgreSQLの起動とデータベースおよびテーブルの作成

上記のプログラムを実行するには、またPostgreSQLを起動し、データベースとテーブルを作成する必要があります:

$ createdb sqlc
$ psql -f schema.sql -d sqlc

最初のコマンドはsqlcという名前のデータベースを作成し、2番目のコマンドはsqlcデータベースでschema.sqlファイルのステートメントを実行し、つまりテーブルを作成します。

プログラムの実行

$ go run .

実行結果の例:

[]
{1 Rob Pike {Co-author of The Go Programming Language true}}

コード生成

sqlcは、SQLステートメント自体に加えて、私たちがSQLステートメントを書く際にコメントの形式で生成されるプログラムに必要ないくつかの基本情報を提供する必要があります。構文は次のとおりです:

-- name: <name> <cmd>

nameは生成されるメソッドの名前で、例えば上記のCreateUserListUsersGetUserDeleteUserなどです。cmdには次の値があります:

  • :one:SQLステートメントが1つのオブジェクトを返すことを示し、生成されるメソッドの返り値は(オブジェクト型, error)で、オブジェクト型はテーブル名から導き出すことができます。
  • :many:SQLステートメントが複数のオブジェクトを返すことを示し、生成されるメソッドの返り値は([]オブジェクト型, error)です。
  • :exec:SQLステートメントがオブジェクトを返さず、ただerrorを返すことを示します。
  • :execrows:SQLステートメントが影響を受けた行数を返す必要があることを示します。

:oneの例

-- name: GetUser :one
SELECT id, name, bio FROM users
WHERE id = $1 LIMIT 1

コメントの--nameGetUserメソッドを生成するよう指示します。テーブル名から導き出すと、返り値の基本型はUserです。:oneはただ1つのオブジェクトが返されることを示します。したがって、最終的な返り値は(User, error)です:

// db/query.sql.go
const getUser = `-- name: GetUser :one
SELECT id, name, bio FROM users
WHERE id = $1 LIMIT 1
`

func (q *Queries) GetUser(ctx context.Context, id int64) (User, error) {
  row := q.db.QueryRowContext(ctx, getUser, id)
  var i User
  err := row.Scan(&i.ID, &i.Name, &i.Bio)
  return i, err
}

:manyの例

-- name: ListUsers :many
SELECT * FROM users
ORDER BY name;

コメントの--nameListUsersメソッドを生成するよう指示します。テーブル名usersから導き出すと、返り値の基本型はUserです。:manyはオブジェクトのスライスが返されることを示します。したがって、最終的な返り値は([]User, error)です:

// db/query.sql.go
const listUsers = `-- name: ListUsers :many
SELECT id, name, bio FROM users
ORDER BY name
`

func (q *Queries) ListUsers(ctx context.Context) ([]User, error) {
  rows, err := q.db.QueryContext(ctx, listUsers)
  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, &i.Bio); 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
}

ここで注意するべき詳細は、私たちがselect *を使用しても、生成されたコードのSQLステートメントは特定のフィールドに書き換えられるということです:

SELECT id, name, bio FROM users
ORDER BY name

このように、後でフィールドを追加または削除する必要がある場合、sqlcコマンドを実行するだけで、このSQLステートメントとListUsers()メソッドを一致させることができ、非常に便利です!

:execの例

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

コメントの--nameDeleteUserメソッドを生成するよう指示します。テーブル名usersから導き出すと、返り値の基本型はUserです。:execはオブジェクトが返されないことを示します。したがって、最終的な返り値はerrorです:

// db/query.sql.go
const deleteUser = `-- name: DeleteUser :exec
DELETE FROM users
WHERE id = $1
`

func (q *Queries) DeleteUser(ctx context.Context, id int64) error {
  _, err := q.db.ExecContext(ctx, deleteUser, id)
  return err
}

:execrowsの例

-- name: DeleteUserN :execrows
DELETE FROM users
WHERE id = $1

コメントの--nameDeleteUserNメソッドを生成するよう指示します。テーブル名usersから導き出すと、返り値の基本型はUserです。:execは影響を受けた行数(つまり削除された行数)が返されることを示します。したがって、最終的な返り値は(int64, error)です:

// db/query.sql.go
const deleteUserN = `-- name: DeleteUserN :execrows
DELETE FROM users
WHERE id = $1
`

func (q *Queries) DeleteUserN(ctx context.Context, id int64) (int64, error) {
  result, err := q.db.ExecContext(ctx, deleteUserN, id)
  if err != nil {
    return 0, err
  }
  return result.RowsAffected()
}

書かれたSQLがどれほど複雑であっても、上記の規則に従います。私たちはSQLステートメントを書く際に、1行のコメントを追加するだけで、sqlcが私たちのために慣習的なSQL操作メソッドを生成してくれます。生成されたコードは、私たちが手で書くものと変わりありませんし、エラーハンドリングも非常に完全であり、手で書く際の面倒とエラーをも回避します。

モデルオブジェクト

sqlcは、すべてのテーブル作成ステートメントに対して対応するモデル構造を生成します。構造名はテーブル名の単数形で、先頭文字は大文字になります。例えば:

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

これに対応する構造を生成します:

type User struct {
  ID   int
  Name string
}

また、sqlcはALTER TABLEステートメントを解析することができ、最終的なテーブル構造に基づいてモデルオブジオブジェクトの構造を生成します。例えば:

CREATE TABLE users (
  id          SERIAL PRIMARY KEY,
  birth_year  int    NOT NULL
);

ALTER TABLE users ADD COLUMN bio text NOT NULL;
ALTER TABLE users DROP COLUMN birth_year;
ALTER TABLE users RENAME TO writers;

上記のSQLステートメントでは、テーブルを作成するときにidbirth_yearの2つのカラムがあります。最初のALTER TABLEステートメントはbioカラムを追加し、2番目はbirth_yearカラムを削除し、3番目はテーブル名をusersからwritersに変更します。sqlcは、最終的なテーブル名writersとテーブル内のidbioのカラムに基づいてコードを生成します:

package db

type Writer struct {
  ID  int
  Bio string
}

設定フィールド

sqlc.yamlファイルでは、他の設定フィールドも設定することができます。

emit_json_tags

デフォルト値はfalseです。このフィールドをtrueに設定すると、生成されるモデルオブジェクト構造にJSONタグを追加することができます。例えば:

CREATE TABLE users (
  id         SERIAL    PRIMARY KEY,
  created_at timestamp NOT NULL
);

これにより、次のように生成されます:

package db

import (
  "time"
)

type User struct {
  ID        int       `json:"id"`
  CreatedAt time.Time `json:"created_at"`
}

emit_prepared_queries

デフォルト値はfalseです。このフィールドをtrueに設定すると、SQLに対応するプリペアドステートメントが生成されます。例えば、クイックスタートの例でこのオプションを設定すると、最終的に生成される構造体Queriesには、SQLに対応するすべてのプリペアドステートメントオブジェクトが追加されます:

type Queries struct {
  db                DBTX
  tx                *sql.Tx
  createUserStmt    *sql.Stmt
  deleteUserStmt    *sql.Stmt
  getUserStmt       *sql.Stmt
  listUsersStmt     *sql.Stmt
}

そして、Prepare()メソッドが生成されます:

func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
  q := Queries{db: db}
  var err error
  if q.createUserStmt, err = db.PrepareContext(ctx, createUser); err != nil {
    return nil, fmt.Errorf("error preparing query CreateUser: %w", err)
  }
  if q.deleteUserStmt, err = db.PrepareContext(ctx, deleteUser); err != nil {
    return nil, fmt.Errorf("error preparing query DeleteUser: %w", err)
  }
  if q.getUserStmt, err = db.PrepareContext(ctx, getUser); err != nil {
    return nil, fmt.Errorf("error preparing query GetUser: %w", err)
  }
  if q.listUsersStmt, err = db.PrepareContext(ctx, listUsers); err != nil {
    return nil, fmt.Errorf("error preparing query ListUsers: %w", err)
  }
  return &q, nil
}

その他の生成されたメソッドはすべて、これらのオブジェクトを使用し、直接SQLステートメントを使用しません:

func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
  row := q.queryRow(ctx, q.createUserStmt, createUser, arg.Name, arg.Bio)
  var i User
  err := row.Scan(&i.ID, &i.Name, &i.Bio)
  return i, err
}

私たちは、プログラムの初期化時にこのPrepare()メソッドを呼び出す必要があります。

emit_interface

デフォルト値はfalseです。このフィールドをtrueに設定すると、クエリ構造体に対するインターフェイスが生成されます。例えば、クイックスタートの例でこのオプションを設定すると、最終的に生成されたコードには、追加のファイルquerier.goがあります:

// db/querier.go
type Querier interface {
  CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
  DeleteUser(ctx context.Context, id int64) error
  DeleteUserN(ctx context.Context, id int64) (int64, error)
  GetUser(ctx context.Context, id int64) (User, error)
  ListUsers(ctx context.Context) ([]User, error)
}

結論

sqlcはまだ完全ではありませんが、確かにGoでのデータベースコードの書き方の複雑さを大幅に単純化し、私たちのコーディング効率を向上させ、エラーの発生率を減らすことができます。PostgreSQLを使用する人には、是非試してみることをおすすめします!

参考文献

Leapcell: The Next-Gen Serverless Platform for Golang Hosting

最後に、Goサービスのデプロイに最適なプラットフォームをおすすめします:Leapcell

barndpic.png

1. 多言語対応

  • JavaScript、Python、Go、またはRustで開発できます。

2. 無料で無制限のプロジェクトをデプロイ

  • 使用した分だけ支払います — リクエストがなければ、請求はありません。

3. 抜群のコスト効率

  • 使った分だけ支払い、アイドル時には請求されません。
  • 例:平均応答時間60msで694万件のリクエストに対応するのに25ドルです。

4. 洗練された開発者体験

  • 直感的なUIで簡単にセットアップできます。
  • 完全自動化されたCI/CDパイプラインとGitOpsの統合。
  • 実時間のメトリクスとロギングによる実行可能なインサイト。

5. 簡単なスケーラビリティと高パフォーマンス

  • 高い同時実行性を簡単に処理できる自動スケーリング。
  • ゼロの運用オーバーヘッド — ただ構築に集中できます。

Frame3-withpadding2x.png

ドキュメントでさらに詳しく調べる!

LeapcellのTwitter:https://x.com/LeapcellHQ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?