Go sqlcデータベース層実践:SQLコード生成で型安全データアクセスを構築する5つのコアパターン
2026年、Goデータベースアクセスはついに「文字列SQL + interface{}スキャン」という原始的な時代から脱却しました。sqlcは極めてエレガントな方法で型安全性の問題を解決します:標準SQLを書けば、型安全なGoコードを生成してくれます。ORMの抽象化リークも、手書きsql.Scanの煩雑さも、実行時リフレクションのパフォーマンスオーバーヘッドもありません。sqlcはコンパイル時にクエリがどのフィールドと型を返すかを知っており、SQLエラーはすべてコード生成段階で発見されます。本記事では5つのコアパターンを通じて、プロダクション級のsqlcデータベースアクセス層を構築します。
コア概念
| 概念 | 説明 | 主要ツール |
|---|---|---|
| sqlc設定 | YAML設定ファイルで生成ルールを定義 | sqlc.yaml |
| SQLクエリアノテーション | SQLファイル内のコメントでクエリメタデータを定義 | -- name: QueryName :one |
| コード生成 | sqlcがSQLを型安全なGoコードにコンパイル | sqlc generate |
| データベースマイグレーション | バージョン管理されたDBスキーマ変更管理 | golang-migrate |
| カスタム型 | Go型とSQL型のマッピングオーバーライド | sqlc.yaml overrides |
問題分析:Goデータベースアクセスの5つのペインポイント
ペインポイント1:手書きSQL文字列に型チェックがない
db.Query("SELECT id, name FROM users WHERE id = $1", id)——テーブル名のタイプミス、カラム名のタイプミス、パラメータ型の不一致、これらのエラーは実行時にしか発見できない。
ペインポイント2:sql.Scanの手動マッピングが煩雑でエラーが起きやすい
各クエリでrows.Scan(&id, &name, &email, &createdAt)を手書きする必要がある。フィールドの順序はSELECTと一致しなければならず、フィールドの追加・削除時に見落としが発生しやすい。
ペインポイント3:ORMの抽象化リークが深刻
GORMのPreload、Joins、Associationは単純なケースでは使いやすいが、複雑なクエリでは生成されるSQLが制御不能になり、N+1問題が頻発し、デバッグが困難。
ペインポイント4:データベースマイグレーションとコードの非同期
マイグレーションスクリプトとGoコードは別の体系であり、スキーマ変更後にGoコードの更新を忘れがちで、ランタイムエラーを引き起こす。
ペインポイント5:トランザクション管理コードの重複
トランザクションが必要な操作ごとにdb.Begin()、tx.Commit()、tx.Rollback()のボイラープレートコードを書く必要があり、Rollbackの見落としは接続リークを引き起こす。
コアパターン1:sqlc設定と基本クエリ生成
sqlcのコアワークフローは極めてシンプル:SQLクエリファイルを書く → sqlc.yamlを設定する → sqlc generateを実行する → 型安全なGoコードを得る。すべてのSQLは標準SQLであり、新しいクエリ言語を学ぶ必要はない。
# sqlc.yaml
version: "2"
sql:
- engine: "postgresql"
queries: "queries/"
schema: "migrations/"
gen:
go:
package: "db"
out: "internal/db"
sql_package: "pgx/v5"
emit_json_tags: true
emit_prepared_queries: true
emit_interface: true
emit_exact_table_names: false
emit_empty_slices: true
overrides:
- db_type: "uuid"
go_type: "github.com/google/uuid.UUID"
- db_type: "timestamptz"
go_type: "time.Time"
- db_type: "text"
go_type: "string"
- column: "users.avatar_url"
go_type:
type: "NullString"
import: "database/sql"
- column: "users.role"
go_type:
type: "UserRole"
import: "toolsku/internal/db/custom"
-- queries/users.sql
-- name: GetUser :one
SELECT id, name, email, role, avatar_url, created_at, updated_at
FROM users
WHERE id = $1;
-- name: GetUserByEmail :one
SELECT id, name, email, role, avatar_url, created_at, updated_at
FROM users
WHERE email = $1;
-- name: ListUsers :many
SELECT id, name, email, role, avatar_url, created_at, updated_at
FROM users
WHERE ($1::text IS NULL OR name ILIKE '%' || $1 || '%')
AND ($2::text IS NULL OR email ILIKE '%' || $2 || '%')
ORDER BY created_at DESC
LIMIT $3 OFFSET $4;
-- name: CountUsers :one
SELECT COUNT(*) FROM users
WHERE ($1::text IS NULL OR name ILIKE '%' || $1 || '%')
AND ($2::text IS NULL OR email ILIKE '%' || $2 || '%');
-- name: CreateUser :one
INSERT INTO users (name, email, role, avatar_url)
VALUES ($1, $2, $3, $4)
RETURNING id, name, email, role, avatar_url, created_at, updated_at;
-- name: UpdateUser :one
UPDATE users
SET name = $2, email = $3, role = $4, avatar_url = $5, updated_at = NOW()
WHERE id = $1
RETURNING id, name, email, role, avatar_url, created_at, updated_at;
-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;
重要ポイント:
-- name: QueryName :one/:many/:execアノテーションでクエリ名と戻り型を定義:oneは単一レコード、:manyはスライス、:execはデータを返さないsql.NullStringでnullableフィールドを処理、ゼロ値の曖昧さを回避RETURNING句でINSERT/UPDATEが完全なレコードを返し、二次クエリを回避
コアパターン2:複雑クエリとJoinコード生成
sqlcのJOINクエリサポートは最も強力な機能の一つです。すべてのJOINフィールドを含む構造体を自動生成し、手動マッピングは不要です。複雑なレポートクエリや集計クエリでも、sqlcは正確な型を生成します。
-- queries/orders.sql
-- name: GetOrderWithDetails :one
SELECT
o.id, o.order_number, o.status, o.total_amount, o.created_at,
u.id AS user_id, u.name AS user_name, u.email AS user_email,
p.id AS product_id, p.name AS product_name, p.price AS product_price,
oi.quantity, oi.unit_price
FROM orders o
INNER JOIN users u ON o.user_id = u.id
INNER JOIN order_items oi ON o.id = oi.order_id
INNER JOIN products p ON oi.product_id = p.id
WHERE o.id = $1;
-- name: ListOrdersWithUser :many
SELECT
o.id, o.order_number, o.status, o.total_amount, o.created_at,
u.id AS user_id, u.name AS user_name, u.email AS user_email
FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE ($1::text IS NULL OR o.status = $1::order_status)
AND ($2::timestamptz IS NULL OR o.created_at >= $2)
AND ($3::timestamptz IS NULL OR o.created_at <= $3)
ORDER BY o.created_at DESC
LIMIT $4 OFFSET $5;
-- name: GetOrderStatistics :one
SELECT
COUNT(*) AS total_orders,
COALESCE(SUM(o.total_amount), 0) AS total_revenue,
COALESCE(AVG(o.total_amount), 0) AS average_order_value,
COUNT(CASE WHEN o.status = 'completed' THEN 1 END) AS completed_orders,
COUNT(CASE WHEN o.status = 'pending' THEN 1 END) AS pending_orders,
COUNT(CASE WHEN o.status = 'cancelled' THEN 1 END) AS cancelled_orders
FROM orders o
WHERE ($1::timestamptz IS NULL OR o.created_at >= $1)
AND ($2::timestamptz IS NULL OR o.created_at <= $2);
-- name: SearchOrders :many
SELECT o.id, o.order_number, o.status, o.total_amount, o.created_at, u.name AS user_name
FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE (o.order_number ILIKE '%' || $1 || '%' OR u.name ILIKE '%' || $1 || '%')
ORDER BY o.created_at DESC
LIMIT $2 OFFSET $3;
重要ポイント:
- sqlcはJOINクエリに対してすべてのフィールドを含む構造体を自動生成、フィールド名にはASエイリアスを使用
- 集計クエリ(COUNT、SUM、AVG)の結果型もsqlcが正確に推論
$1::text IS NULLパターンでオプションのフィルタ条件を実現、1つのクエリで複数のシナリオをカバーpgtype.NumericでPostgreSQLのNUMERIC型を処理、精度損失を防止
コアパターン3:トランザクション管理とデータベースマイグレーション
sqlcが生成するQueries構造体は、それ自体は接続やトランザクションを管理しません。sqlcのemit_interfaceオプションでQuerierインターフェースを生成し、カスタムトランザクションマネージャーと組み合わせることで、エレガントなトランザクション処理を実現します。
// internal/db/tx.go - トランザクションマネージャー
package db
import (
"context"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// Store データベースストレージ層(接続プールとトランザクションをラップ)
type Store struct {
db *pgxpool.Pool
*Queries
}
// NewStore Storeを作成
func NewStore(db *pgxpool.Pool) *Store {
return &Store{
db: db,
Queries: New(db),
}
}
// ExecTx トランザクション内で操作を実行
func (s *Store) ExecTx(ctx context.Context, fn func(*Queries) error) error {
tx, err := s.db.Begin(ctx)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer func() {
if err != nil {
if rbErr := tx.Rollback(ctx); rbErr != nil {
err = fmt.Errorf("tx error: %v, rollback error: %w", err, rbErr)
}
}
}()
q := New(tx)
if err = fn(q); err != nil {
return err
}
return tx.Commit(ctx)
}
// TransferTx 送金トランザクションを実行
func (s *Store) TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error) {
var result TransferTxResult
err := s.ExecTx(ctx, func(q *Queries) error {
var err error
result.Transfer, err = q.CreateTransfer(ctx, CreateTransferParams{
FromAccountID: arg.FromAccountID,
ToAccountID: arg.ToAccountID,
Amount: arg.Amount,
})
if err != nil {
return fmt.Errorf("create transfer: %w", err)
}
result.FromEntry, err = q.CreateEntry(ctx, CreateEntryParams{
AccountID: arg.FromAccountID, Amount: -arg.Amount,
})
if err != nil {
return fmt.Errorf("create from entry: %w", err)
}
result.ToEntry, err = q.CreateEntry(ctx, CreateEntryParams{
AccountID: arg.ToAccountID, Amount: arg.Amount,
})
if err != nil {
return fmt.Errorf("create to entry: %w", err)
}
result.FromAccount, err = q.AddAccountBalance(ctx, AddAccountBalanceParams{
ID: arg.FromAccountID, Amount: -arg.Amount,
})
if err != nil {
return fmt.Errorf("update from account: %w", err)
}
result.ToAccount, err = q.AddAccountBalance(ctx, AddAccountBalanceParams{
ID: arg.ToAccountID, Amount: arg.Amount,
})
if err != nil {
return fmt.Errorf("update to account: %w", err)
}
return nil
})
return result, err
}
// CreateOrderTx 注文トランザクションを作成
func (s *Store) CreateOrderTx(ctx context.Context, orderParams CreateOrderParams, items []CreateOrderItemParams) (Order, error) {
var order Order
err := s.ExecTx(ctx, func(q *Queries) error {
var err error
order, err = q.CreateOrder(ctx, orderParams)
if err != nil {
return fmt.Errorf("create order: %w", err)
}
for _, item := range items {
item.OrderID = order.ID
if _, err = q.CreateOrderItem(ctx, item); err != nil {
return fmt.Errorf("create order item: %w", err)
}
}
for _, item := range items {
if err = q.DecrementProductStock(ctx, DecrementProductStockParams{
ID: item.ProductID, Quantity: item.Quantity,
}); err != nil {
return fmt.Errorf("decrement stock: %w", err)
}
}
return nil
})
return order, err
}
-- migrations/001_create_users.up.sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('user', 'admin', 'moderator')),
avatar_url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_name ON users(name);
-- migrations/001_create_users.down.sql
DROP TABLE IF EXISTS users;
重要ポイント:
Storeは接続プールとQueriesをラップし、トランザクション管理機能を提供ExecTxはクロージャ関数を受け取り、Begin/Commit/Rollbackを自動処理- クロージャ内で
New(tx)を使用してトランザクションにバインドされたQueriesを作成、すべての操作が同じトランザクションで実行 embed.FSでマイグレーションファイルを埋め込み、単一バイナリにコンパイル
コアパターン4:カスタム型とEnumマッピング
sqlcのデフォルト生成型は時に不正確です。例えば、PostgreSQLのENUM型はデフォルトでstringにマッピングされます。sqlc.yamlのoverridesとemit_enum設定で、正確な型マッピングを実現できます。
# sqlc.yaml - カスタム型設定
version: "2"
sql:
- engine: "postgresql"
queries: "queries/"
schema: "migrations/"
gen:
go:
package: "db"
out: "internal/db"
sql_package: "pgx/v5"
emit_json_tags: true
emit_enum_valid_method: true
emit_all_enum_values: true
overrides:
- db_type: "uuid"
go_type: "github.com/google/uuid.UUID"
- db_type: "timestamptz"
go_type: "time.Time"
- db_type: "order_status"
go_type:
type: "OrderStatus"
import: "toolsku/internal/db/custom"
- db_type: "user_role"
go_type:
type: "UserRole"
import: "toolsku/internal/db/custom"
- db_type: "numeric"
go_type:
type: "Decimal"
import: "github.com/shopspring/decimal"
// internal/db/custom/types.go - カスタム型定義
package custom
import (
"database/sql/driver"
"fmt"
)
// UserRole ユーザーロールEnum
type UserRole string
const (
UserRoleUser UserRole = "user"
UserRoleAdmin UserRole = "admin"
UserRoleModerator UserRole = "moderator"
)
func (r UserRole) Valid() bool {
switch r {
case UserRoleUser, UserRoleAdmin, UserRoleModerator:
return true
}
return false
}
func (r UserRole) Value() (driver.Value, error) {
if !r.Valid() {
return nil, fmt.Errorf("invalid user role: %s", r)
}
return string(r), nil
}
func (r *UserRole) Scan(value interface{}) error {
if value == nil {
*r = UserRoleUser
return nil
}
s, ok := value.(string)
if !ok {
return fmt.Errorf("cannot scan %T into UserRole", value)
}
*r = UserRole(s)
if !r.Valid() {
return fmt.Errorf("invalid user role: %s", s)
}
return nil
}
// OrderStatus 注文ステータスEnum
type OrderStatus string
const (
OrderStatusPending OrderStatus = "pending"
OrderStatusProcessing OrderStatus = "processing"
OrderStatusCompleted OrderStatus = "completed"
OrderStatusCancelled OrderStatus = "cancelled"
)
func (s OrderStatus) Valid() bool {
switch s {
case OrderStatusPending, OrderStatusProcessing, OrderStatusCompleted, OrderStatusCancelled:
return true
}
return false
}
func (s OrderStatus) Value() (driver.Value, error) {
if !s.Valid() {
return nil, fmt.Errorf("invalid order status: %s", s)
}
return string(s), nil
}
func (s *OrderStatus) Scan(value interface{}) error {
if value == nil {
*s = OrderStatusPending
return nil
}
v, ok := value.(string)
if !ok {
return fmt.Errorf("cannot scan %T into OrderStatus", value)
}
*s = OrderStatus(v)
if !s.Valid() {
return fmt.Errorf("invalid order status: %s", v)
}
return nil
}
重要ポイント:
emit_enum_valid_method: trueでEnumにValid()メソッドを生成emit_all_enum_values: trueでEnumのすべての値の定数を生成- カスタム型は
driver.Valuerとsql.Scannerインターフェースを実装する必要がある shopspring/decimalでNUMERIC型を処理、float64の精度損失を防止
コアパターン5:プロダクション級データベースアクセス層のカプセル化
sqlc生成コードをプロダクション級のデータアクセス層にカプセル化し、接続プール管理、ヘルスチェック、タイムアウト制御、リトライロジックなどのプロダクション級機能を追加します。
// internal/db/store.go - プロダクション級データベースストレージ層
package db
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
// StoreConfig ストレージ層設定
type StoreConfig struct {
DatabaseURL string
MaxConns int32
MinConns int32
MaxConnLifetime time.Duration
MaxConnIdleTime time.Duration
HealthCheckPeriod time.Duration
}
func DefaultStoreConfig(url string) StoreConfig {
return StoreConfig{
DatabaseURL: url, MaxConns: 25, MinConns: 5,
MaxConnLifetime: 30 * time.Minute, MaxConnIdleTime: 5 * time.Minute,
HealthCheckPeriod: 1 * time.Minute,
}
}
// Store プロダクション級データベースストレージ層
type Store struct {
pool *pgxpool.Pool
*Queries
}
func NewStore(ctx context.Context, cfg StoreConfig) (*Store, error) {
poolConfig, err := pgxpool.ParseConfig(cfg.DatabaseURL)
if err != nil {
return nil, fmt.Errorf("parse database url: %w", err)
}
poolConfig.MaxConns = cfg.MaxConns
poolConfig.MinConns = cfg.MinConns
poolConfig.MaxConnLifetime = cfg.MaxConnLifetime
poolConfig.MaxConnIdleTime = cfg.MaxConnIdleTime
poolConfig.HealthCheckPeriod = cfg.HealthCheckPeriod
pool, err := pgxpool.NewWithConfig(ctx, poolConfig)
if err != nil {
return nil, fmt.Errorf("create connection pool: %w", err)
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("ping database: %w", err)
}
return &Store{pool: pool, Queries: New(pool)}, nil
}
func (s *Store) Close() { s.pool.Close() }
func (s *Store) Health(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
return s.pool.Ping(ctx)
}
func (s *Store) Stats() *pgxpool.Stat { return s.pool.Stat() }
// UserService ユーザーサービス層
type UserService struct {
store *Store
}
func NewUserService(store *Store) *UserService {
return &UserService{store: store}
}
// ListUsersInput リストクエリ入力
type ListUsersInput struct {
Search string
Page int32
Size int32
}
// ListUsersOutput リストクエリ出力
type ListUsersOutput struct {
Users []User `json:"users"`
Total int64 `json:"total"`
Page int32 `json:"page"`
PageSize int32 `json:"page_size"`
}
// List ユーザーリストを取得(ページネーションと検索付き)
func (s *UserService) List(ctx context.Context, input ListUsersInput) (*ListUsersOutput, error) {
if input.Page < 1 { input.Page = 1 }
if input.Size < 1 || input.Size > 100 { input.Size = 20 }
offset := (input.Page - 1) * input.Size
var searchName, searchEmail sql.NullString
if input.Search != "" {
searchName = sql.NullString{String: input.Search, Valid: true}
searchEmail = sql.NullString{String: input.Search, Valid: true}
}
count, err := s.store.CountUsers(ctx, CountUsersParams{Name: searchName, Email: searchEmail})
if err != nil {
return nil, fmt.Errorf("count users: %w", err)
}
users, err := s.store.ListUsers(ctx, ListUsersParams{
Name: searchName, Email: searchEmail, Limit: input.Size, Offset: offset,
})
if err != nil {
return nil, fmt.Errorf("list users: %w", err)
}
return &ListUsersOutput{
Users: users, Total: count, Page: input.Page, PageSize: input.Size,
}, nil
}
// Create ユーザーを作成(メール一意性チェック付き)
func (s *UserService) Create(ctx context.Context, params CreateUserParams) (*User, error) {
_, err := s.store.GetUserByEmail(ctx, params.Email)
if err == nil {
return nil, fmt.Errorf("email already exists: %s", params.Email)
}
if err != sql.ErrNoRows {
return nil, fmt.Errorf("check email: %w", err)
}
user, err := s.store.CreateUser(ctx, params)
if err != nil {
return nil, fmt.Errorf("create user: %w", err)
}
return &user, nil
}
// Update ユーザーを更新
func (s *UserService) Update(ctx context.Context, id uuid.UUID, params UpdateUserParams) (*User, error) {
_, err := s.store.GetUser(ctx, id)
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
params.ID = id
user, err := s.store.UpdateUser(ctx, params)
if err != nil {
return nil, fmt.Errorf("update user: %w", err)
}
return &user, nil
}
// Delete ユーザーを削除
func (s *UserService) Delete(ctx context.Context, id uuid.UUID) error {
_, err := s.store.GetUser(ctx, id)
if err != nil {
return fmt.Errorf("user not found: %w", err)
}
return s.store.DeleteUser(ctx, id)
}
重要ポイント:
pgxpool接続プール設定(MaxConns、MinConns、タイムアウト)はプロダクション環境に不可欠UserServiceがビジネスロジックをカプセル化、HandlerはHTTPプロトコルにのみ注力embed.FSでマイグレーションファイルを埋め込み、デプロイ時に追加ファイル不要
よくある落とし穴
落とし穴1:sqlc生成コードを手動で修正
# ❌ 間違い:sqlc生成コードを手動で修正、次回generateで上書きされる
vim internal/db/users.sql.go
# ✅ 正しい:すべての変更をSQLファイルまたはカスタム型で行い、再生成
vim queries/users.sql
sqlc generate
落とし穴2:nullableフィールドにNull型ではなくゼロ値を使用
// ❌ 間違い:nullableフィールドにstringのゼロ値を使用、NULLと空文字を区別できない
type User struct {
AvatarUrl string `json:"avatar_url"` // NULLが""になる
}
// ✅ 正しい:sql.NullStringまたはポインタ型を使用
type User struct {
AvatarUrl sql.NullString `json:"avatar_url"` // NULLはValid:false
}
落とし穴3:トランザクションでtx作成Queriesの使用を忘れる
// ❌ 間違い:トランザクションでグローバルQueriesを使用、操作が同じトランザクションにない
func (s *Store) TransferTx(ctx context.Context) error {
tx, _ := s.db.Begin(ctx)
defer tx.Rollback(ctx)
s.CreateTransfer(ctx, ...) // トランザクション内にない!
return tx.Commit(ctx)
}
// ✅ 正しい:New(tx)でトランザクションにバインドされたQueriesを作成
func (s *Store) TransferTx(ctx context.Context) error {
tx, _ := s.db.Begin(ctx)
defer tx.Rollback(ctx)
q := New(tx)
q.CreateTransfer(ctx, ...) // トランザクション内
return tx.Commit(ctx)
}
落とし穴4:sqlc.yamlのoverridesパスが間違っている
# ❌ 間違い:columnパスにschema.tableなし
overrides:
- column: "avatar_url"
go_type: "*string"
# ✅ 正しい:完全なschema.table.columnパスを使用
overrides:
- column: "public.users.avatar_url"
go_type: "*string"
落とし穴5:マイグレーションファイルの命名が非標準
# ❌ 間違い:マイグレーションファイル名にシーケンスプレフィックスがない
migrations/create_users.up.sql
# ✅ 正しい:シーケンスプレフィックスを使用して実行順序を確保
migrations/001_create_users.up.sql
migrations/001_create_users.down.sql
エラートラブルシューティング
| エラー現象 | 可能な原因 | トラブルシューティング方法 | 解決策 |
|---|---|---|---|
| sqlc generateエラー | SQL構文エラー | queries/ディレクトリのSQLを確認 | SQL構文を修正 |
| 生成コードの型不一致 | overrides設定エラー | sqlc.yamlのoverridesを確認 | 正しいcolumn/db_typeパスを使用 |
| ランタイムScanエラー | SELECTフィールドと構造体が不一致 | SQLのSELECTリストを確認 | SELECTに必要なフィールドがすべて含まれていることを確認 |
| 接続プール枯渇 | MaxConnsが小さすぎる | pool.Stats()を確認 | MaxConnsを増やすかスロークエリを調査 |
| トランザクションタイムアウト | トランザクション実行時間が長すぎる | トランザクション内の操作を確認 | contextタイムアウトを追加するかトランザクションを分割 |
| マイグレーション競合 | 複数インスタンスが同時にマイグレーション | migrateのロック機構を確認 | migrateのadvisory lockを使用 |
| NULL値でpanic | nullableフィールドにNull型を使用していない | フィールド定義を確認 | sql.NullStringまたはポインタを使用 |
| Enum値の検証失敗 | DB値がEnum範囲外 | データベースデータを確認 | DB CHECK制約を追加 |
| JOIN結果のフィールド欠落 | SELECTにJOINフィールドが含まれていない | SQLのSELECTを確認 | 欠落しているJOINフィールドを追加 |
| pgtype.Numericの精度損失 | NUMERICをfloat64で読み取り | overrides設定を確認 | shopspring/decimalを使用 |
高度な最適化
1. sqlc検証モード
CIでsqlc vetを使用してSQLクエリの品質をチェック。SELECT *、LIMIT欠落、N+1クエリなどのパフォーマンス問題を検出。
2. バッチ操作の最適化
pgxのCopyFromでバルクインサートを実現——個別INSERTより10-100倍高速。sqlcはpgxのbatch queryモードをサポート。
3. 読み取り/書き込み分離
プライマリ-レプリカデータベース接続プールを設定。書き込み操作はプライマリ、読み取り操作はレプリカにルーティング。Storeで読み取り/書き込みルーティングロジックをカプセル化。
4. クエリキャッシュ層
UserService層にRedisキャッシュを追加。ホットクエリ(ユーザー情報など)はキャッシュを先に確認し、その後データベースにアクセスしてDB負荷を軽減。
5. スロークエリ監視
pgxのConnConfig.Tracerで全SQL実行時間を記録。閾値を超えるクエリは自動アラート。OpenTelemetryと組み合わせてフルチェーントレーシングを実現。
比較
| 次元 | sqlc | GORM | sqlx | Ent |
|---|---|---|---|---|
| 型安全性 | ✅ コンパイル時チェック | ⚠️ ランタイムチェック | ❌ なし | ✅ コード生成 |
| SQL制御 | ✅ 完全制御 | ❌ ORM生成 | ✅ 完全制御 | ❌ 抽象化層 |
| 学習曲線 | ✅ SQLのみ | ⚠️ ORM概念 | ✅ SQLのみ | ⚠️ グラフAPI |
| コード生成 | ✅ 自動生成 | ❌ リフレクション | ❌ 手書き | ✅ 自動生成 |
| JOINサポート | ✅ ネイティブSQL | ⚠️ Preload | ✅ ネイティブSQL | ✅ グラフトラバーサル |
| マイグレーション管理 | ❌ 外部ツールが必要 | ✅ AutoMigrate | ❌ 外部ツールが必要 | ✅ 内蔵 |
| パフォーマンス | ✅ プリペアドステートメント | ⚠️ リフレクションオーバーヘッド | ✅ ネイティブ | ⚠️ 抽象化オーバーヘッド |
| デバッグ難易度 | ✅ SQLが可視 | ❌ 生成SQLが制御不能 | ✅ SQLが可視 | ⚠️ 抽象化層 |
まとめ
sqlcは最もシンプルな方法でGoデータベースアクセスの型安全性の問題を解決します:SQLを書けば、Goコードを生成してくれます。ORMの抽象化リークも、手書きScanの煩雑さも、実行時リフレクションのパフォーマンスオーバーヘッドもありません。複雑なクエリが必要な時はSQLを直接書き、型安全性が必要な時はsqlcが生成します。SQLはデータベースの共通言語であり、sqlcはGo開発者が最も自然な方法でデータベースと対話できるようにします——それがsqlcの哲学です。
オンラインツールおすすめ
- JSONフォーマッター — sqlc生成のJSONタグ構造をフォーマットし、データモデルを素早く検証
- cURL to Code — cURLコマンドをGo HTTPコードに変換し、APIエンドポイントを素早くテスト
- ハッシュ計算 — マイグレーションファイルのハッシュ値を計算し、マイグレーションの完全性を検証
ブラウザローカルツールを無料で試す →