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.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}}
コード生成
sqlcは、SQLステートメント自体に加えて、私たちがSQLステートメントを書く際にコメントの形式で生成されるプログラムに必要ないくつかの基本情報を提供する必要があります。構文は次のとおりです:
-- name: <name> <cmd>
name
は生成されるメソッドの名前で、例えば上記のCreateUser
、ListUsers
、GetUser
、DeleteUser
などです。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
コメントの--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ステートメントを書く際に、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ステートメントでは、テーブルを作成するときにid
とbirth_year
の2つのカラムがあります。最初の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: The Next-Gen Serverless Platform for Golang Hosting
最後に、Goサービスのデプロイに最適なプラットフォームをおすすめします:Leapcell
1. 多言語対応
- JavaScript、Python、Go、またはRustで開発できます。
2. 無料で無制限のプロジェクトをデプロイ
- 使用した分だけ支払います — リクエストがなければ、請求はありません。
3. 抜群のコスト効率
- 使った分だけ支払い、アイドル時には請求されません。
- 例:平均応答時間60msで694万件のリクエストに対応するのに25ドルです。
4. 洗練された開発者体験
- 直感的なUIで簡単にセットアップできます。
- 完全自動化されたCI/CDパイプラインとGitOpsの統合。
- 実時間のメトリクスとロギングによる実行可能なインサイト。
5. 簡単なスケーラビリティと高パフォーマンス
- 高い同時実行性を簡単に処理できる自動スケーリング。
- ゼロの運用オーバーヘッド — ただ構築に集中できます。
LeapcellのTwitter:https://x.com/LeapcellHQ