sqlcでGoのタイプセーフなSQL
Min-jun Kim
Dev Intern · Leapcell

はじめに
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.sql
とquery.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}}
コード生成
SQLステートメント自体に加えて、sqlcはSQLステートメントを記述するときに、コメントの形式で生成されたプログラムの基本情報を提供する必要があります。構文は次のとおりです:
-- name: <name> <cmd>
name
は、生成されたメソッドの名前です。上記のCreateUser
、ListUsers
、GetUser
、DeleteUser
など。cmd
は次の値を持つことができます:
:one
: SQLステートメントが1つのオブジェクトを返すことを示します。生成されたメソッドの戻り値は(オブジェクト型、エラー)
で、オブジェクト型はテーブル名から派生できます。:many
: SQLステートメントが複数のオブジェクトを返すことを示します。生成されたメソッドの戻り値は([]オブジェクト型、エラー)
です。:exec
: SQLステートメントがオブジェクトを返さず、error
のみを返すことを示します。:execrows
: SQLステートメントが影響を受けた行数を返す必要があることを示します。
:one
の例
-- name: GetUser :one SELECT id, name, bio FROM users WHERE id = $1 LIMIT 1
コメントの--name
は、メソッドGetUser
を生成するように指示します。テーブル名から派生して、戻り値の基本型は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;
コメントの--name
は、メソッドListUsers
を生成するように指示します。テーブル名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
コメントの--name
は、メソッドDeleteUser
を生成するように指示します。テーブル名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
コメントの--name
は、メソッドDeleteUserN
を生成するように指示します。テーブル名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ステートメントを記述するときに余分なコメント行を追加するだけで、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ステートメントでは、テーブルが作成されたときに2つの列id
とbirth_year
があります。最初のALTER TABLE
ステートメントは列bio
を追加し、2番目のステートメントはbirth_year
列を削除し、3番目のステートメントはテーブル名をusers
からwriters
に変更します。sqlcは、最終的なテーブル名writers
とテーブル内の列id
およびbio
に従ってコードを生成します:
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: Golangホスティングのための次世代サーバーレスプラットフォーム
最後に、Goサービスのデプロイに最適なプラットフォームをお勧めします:Leapcell
1. 多言語サポート
- JavaScript、Python、Go、またはRustで開発します。
2. 無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払い - リクエストも料金もかかりません。
3. 比類のないコスト効率
- アイドル料金なしの従量課金制。
- 例:25ドルで平均応答時間60msで694万リクエストをサポートします。
4. 合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI
- 完全に自動化されたCI/CDパイプラインとGitOps統合。
- 実用的な洞察のためのリアルタイムのメトリクスとロギング。
5. 簡単なスケーラビリティと高パフォーマンス
- 高い同時実行性を容易に処理するための自動スケーリング。
- 運用上のオーバーヘッドなし - 構築に集中するだけです。
Leapcell Twitter: https://x.com/LeapcellHQ