مرکزی مواد پر جائیں
友田 陽大
Go & Echo in production
Go
Echo
PostgreSQL
型安全
アーキテクチャ設計
セキュリティ

Echo × データベース本番設計:pgx・sqlc・GORM の選び方、コネクションプール、トランザクション境界、context 伝播

Go Echo(v5)のデータベース層を本番品質で設計する実装ガイド。pgx・sqlc・GORM の選定、pgxpool のコネクションプール調整、c.Request().Context() を貫通させる context 伝播、安全なトランザクション境界(WithTx)、Repository パターン、SQLインジェクション対策、マイグレーション、サーバーレスの接続枯渇までを実コードで解説します。

Published
Reading time
10 min read
Author
友田 陽大
شیئر کریں

API の信頼性は、最終的にデータベース層の設計で決まります。コネクションプールを誤れば高負荷で枯渇し、トランザクション境界を誤れば不整合が残り、context を渡し忘れればクライアントが切断したのに DB は処理を続ける——こうした事故は「動くデモ」では見えず、本番のピーク時に初めて牙を剥きます。

この記事は、Go Echo 本番運用ガイドのデータベース編です。Echo の役割は薄い HTTP 層なので、DB 設計の大半は Go 標準と各ドライバの作法です。本記事は「Echo ハンドラから DB まで、何をどう繋ぐか」を、型安全・回復性・セキュリティの観点で実装します。

この記事のルール:Echo の API は 公式ドキュメント(v5・2026年6月時点) に基づきます。DB ライブラリ(pgx v5 / sqlc / GORM v2 / golang-migrate)は活発に更新されるため、本番投入前に各公式で最新 API を確認してください。接続情報(DSN・認証情報)は環境変数/シークレットマネージャ前提で、コードやイメージに焼き込みません。


1. 最重要:context をハンドラから DB まで貫通させる

Echo ハンドラから DB を呼ぶとき、必ず c.Request().Context() を渡します。これが本記事で唯一「Echo 特有」かつ最も効くポイントです。

func (h *UserHandler) Get(c *echo.Context) error {
	ctx := c.Request().Context() // ← リクエストの context を取り出す

	user, err := h.repo.FindByID(ctx, c.Param("id")) // ← DB まで渡す
	if err != nil {
		return err
	}
	return c.JSON(http.StatusOK, user)
}

なぜ重要か。この ctx にはリクエストのライフサイクルが乗っています。

  • クライアント切断:ブラウザが離脱すると ctx がキャンセルされ、進行中のクエリも中断される。切断された処理が DB コネクションを占有し続ける事故を防ぐ。
  • タイムアウトcontext.WithTimeout を噛ませれば、遅いクエリを期限で打ち切れる(v5 で middleware.Timeout が削除されたいま、タイムアウトは context で表現する)。
  • トレース伝播:OpenTelemetry のトレース ID が ctx を通って DB スパンまで繋がり、可観測性が一本の糸になる。
// 重いクエリには期限を付ける
ctx, cancel := context.WithTimeout(c.Request().Context(), 3*time.Second)
defer cancel()
rows, err := pool.Query(ctx, "SELECT ...")

context.Background() を DB 呼び出しに使うのはアンチパターンです。リクエスト由来の ctx を捨てた瞬間、上記の恩恵をすべて失います。


2. ライブラリ選定:pgx / sqlc / GORM を損益で選ぶ

「とりあえず GORM」で始めて後悔する、はよくある話です。3つの選択肢をで選びます。

観点database/sql + pgxsqlcGORM
抽象度低(生 SQL)低(生 SQL + 型生成)高(ORM)
型安全手動マッピングSQL から Go を自動生成(最強)構造体タグ依存
学習コスト低〜中(独自 DSL)
パフォーマンス○(リフレクション)
複雑クエリ◎(SQL そのまま)◎(SQL そのまま)△(生 SQL へ退避しがち)
向く場面細かい制御・高性能型安全と SQL の両立CRUD 中心・開発速度優先

判断の目安

  • 型安全を最重視するなら sqlc.sql ファイルに書いたクエリから、引数・戻り値が型付けされた Go コードが生成されます。「SQL を書く自由」と「コンパイル時の型保証」を両立でき、本サイトが掲げる型安全の規律と最も相性が良い選択です。
  • 低レベルな制御・最高性能が要るなら pgx(PostgreSQL 専用ドライバ)。
  • CRUD 中心で開発速度優先なら GORM。ただし複雑クエリで結局生 SQL に逃げるなら、最初から sqlc/pgx の方が一貫します。

3. pgx:コネクションプールを正しく設定する

pgxpool は本番で必須のコネクションプールです。プールの上限設計を誤ると、高負荷時に too many connections でサービスが落ちます。

import "github.com/jackc/pgx/v5/pgxpool"

func NewPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
	cfg, err := pgxpool.ParseConfig(dsn) // DSN は os.Getenv("DATABASE_URL")
	if err != nil {
		return nil, err
	}
	cfg.MaxConns = 10                       // このインスタンスが握る最大接続数
	cfg.MinConns = 2                        // 常時温めておく最小接続数
	cfg.MaxConnLifetime = time.Hour         // 接続を作り直す周期(DB側の制限対策)
	cfg.MaxConnIdleTime = 30 * time.Minute  // アイドル接続の回収
	cfg.HealthCheckPeriod = time.Minute     // 死んだ接続の検出

	return pgxpool.NewWithConfig(ctx, cfg)
}

MaxConns の決め方DB の max_connections全アプリインスタンス数で割り、管理接続分の余裕を引いた値が上限です。例:max_connections=100、アプリ 4 インスタンス、予約 20 なら (100 - 20) / 4 = 20 が天井。ここを「とりあえず大きく」すると、オートスケールで接続が一気に増えて DB が先に死にます。

プールはアプリ起動時に一度だけ生成し、Echo ハンドラ間で共有します。lifespan 相当の初期化は main で行い、defer pool.Close()グレースフルに閉じます

3.1 型安全に近い行マッピング(pgx v5)

pgx v5 は、行を構造体に詰めるヘルパを備えます。手書きの rows.Scan 地獄を避けられます。

type User struct {
	ID    string `db:"id"`
	Name  string `db:"name"`
	Email string `db:"email"`
}

func (r *UserRepo) FindByID(ctx context.Context, id string) (*User, error) {
	rows, err := r.pool.Query(ctx, `SELECT id, name, email FROM users WHERE id = $1`, id)
	if err != nil {
		return nil, err
	}
	user, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[User])
	if errors.Is(err, pgx.ErrNoRows) {
		return nil, ErrUserNotFound // ドメインエラーへ変換(後述)
	}
	return &user, err
}

4. トランザクション境界:WithTx ヘルパに集約する

トランザクションは「複数の書き込みを全か無かにする」ための機構です。Begin / Commit / Rollback を各所に手書きすると、rollback の書き忘れで接続がリークします。1つのヘルパに集約します。

// fn の中の処理を1トランザクションで実行する。エラー/panic なら rollback
func (r *Store) WithTx(ctx context.Context, fn func(tx pgx.Tx) error) error {
	tx, err := r.pool.Begin(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback(ctx) // commit 済みなら no-op。panic 時も確実に巻き戻る

	if err := fn(tx); err != nil {
		return err // defer の Rollback が走る
	}
	return tx.Commit(ctx)
}

使う側は、トランザクションの意図だけを書けます。

func (uc *OrderUseCase) Place(ctx context.Context, order Order) error {
	return uc.store.WithTx(ctx, func(tx pgx.Tx) error {
		if err := insertOrder(ctx, tx, order); err != nil {
			return err
		}
		return decrementStock(ctx, tx, order.Items) // どちらか失敗すれば両方巻き戻る
	})
}

冪等性は DB に担保させる:リトライや二重送信に対しては、アプリのフラグ管理より**一意制約(unique index)**が堅牢です。INSERT ... ON CONFLICT DO NOTHING や冪等キー列で「同じ操作は1回だけ」を DB の制約として表現します。決済のように厳密さが要る領域での冪等設計は、二重課金ゼロの決済信頼性で深掘りしています。


5. Repository パターン:DB をインターフェースの裏に隠す

UseCase 層が pgx や GORM を直接知ると、DB を差し替えられず、テストに本物の DB が要ります。Repository をインターフェースで定義し、実装を裏に隠します。これはクリーンアーキテクチャ + DIの中核でもあります。

// ドメイン層:インターフェース(DB 技術を知らない)
type UserRepository interface {
	FindByID(ctx context.Context, id string) (*User, error)
	Create(ctx context.Context, u *User) error
}

// インフラ層:pgx 実装(差し替え可能)
type pgUserRepository struct {
	pool *pgxpool.Pool
}

func NewUserRepository(pool *pgxpool.Pool) UserRepository {
	return &pgUserRepository{pool: pool}
}

これで UseCase は UserRepository にのみ依存し、テストではモックに差し替え、本番では pgx 実装を注入する——という疎結合が成立します。


6. セキュリティ:SQLインジェクションを構造的に潰す

外部入力を SQL に文字列連結してはいけません。 必ずプレースホルダ($1, $2…)を使います。これは「気をつける」ではなく「そうとしか書けない構造にする」のが正解です。

// ❌ 絶対にやらない:文字列連結(SQLインジェクション)
q := "SELECT * FROM users WHERE email = '" + email + "'" // 攻撃者が ' OR '1'='1 を注入できる

// ✅ プレースホルダ(ドライバが値を安全にエスケープ/バインド)
rows, err := pool.Query(ctx, "SELECT * FROM users WHERE email = $1", email)

ORDER BY のカラム名のようにプレースホルダが使えない箇所は、ユーザー入力を直接埋めず、**許可リスト(allowlist)**で受けます。

allowed := map[string]string{"created_at": "created_at", "name": "name"}
col, ok := allowed[c.QueryParam("sort")]
if !ok {
	col = "created_at" // 既定値。未知の入力は SQL に渡さない
}
query := fmt.Sprintf("SELECT ... ORDER BY %s", col) // col は allowlist 由来なので安全

入力の検証はバインディング&バリデーションで境界に効かせ、SQL 層では「もう信用できる値しか来ない」状態を二重に守ります。


7. GORM を選んだ場合の本番注意点

GORM を使うなら、最低限この2つは押さえます。

import (
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})

// ① プールは内部の *sql.DB で設定する(GORM 自体には無い)
sqlDB, _ := db.DB()
sqlDB.SetMaxOpenConns(10)
sqlDB.SetMaxIdleConns(2)
sqlDB.SetConnMaxLifetime(time.Hour)

// ② context を必ず渡す(WithContext)。N+1 は Preload で潰す
var users []User
err = db.WithContext(ctx).Preload("Orders").Find(&users).Error // 関連を1回でまとめて取得

GORM の最大の罠は N+1 問題(一覧取得後に関連を1件ずつ引いて N 回追加クエリが飛ぶ)です。PreloadJoinsまとめて取得します。WithContext(ctx) を忘れると第1章の恩恵を失う点も同じです。


8. マイグレーション:スキーマもコードで管理する

スキーマ変更は手動 SQL ではなく、バージョン管理されたマイグレーションで行います(golang-migrate / goose が定番)。

# golang-migrate:up/down のペアでバージョンを進める
migrate -database "$DATABASE_URL" -path ./migrations up

CI でマイグレーションの適用と整合性を検証し、本番デプロイ前に流す運用にします。無停止でのスキーマ進化(後方互換を保った段階的変更)は、ゼロダウンタイム・マイグレーションの設計(Python 例だが原則は共通)が体系的です。


9. サーバーレス/オートスケールでの接続枯渇

Lambda や Cloud Run のようにインスタンスが多数・短命な環境では、各インスタンスがプールを持つと接続数が爆発します。max_connections を即座に食い潰し、DB が落ちます。

  • 常駐サーバー(ECS/Cloud Run min>0)pgxpool でプールを共有すれば素直に機能する。
  • Lambda 等の関数:接続プロキシ(RDS Proxy 等)を挟み、関数側はプロキシに繋ぐ。コネクションの多重化はプロキシに任せる。

この罠と対策は、Lambda × RDS/Aurora の接続管理(RDS Proxy)で詳説しています。Echo を ECS/Fargate で常駐運用するなら通常は pgxpool で十分で、これが Echo を「関数」ではなく「常駐サーバー」で動かす利点でもあります。


まとめ:DB 層を本番品質にする7原則

  1. c.Request().Context() を全クエリに貫通させる。context.Background() を DB に使わない。
  2. 型安全重視なら sqlc、制御なら pgx、速度なら GORM——損益で選ぶ。
  3. pgxpoolMaxConns は DB 上限÷インスタンス数から逆算。大きすぎる設定が DB を殺す。
  4. トランザクションは WithTx に集約defer tx.Rollback で巻き戻しを構造化する。
  5. Repository をインターフェースで隠す。DB 差し替えとテスト容易性を両立。
  6. 必ずプレースホルダ。連結禁止。プレースホルダ不可の箇所は allowlist。
  7. 冪等性は一意制約で DB に担保させ、サーバーレスでは接続プロキシで枯渇を防ぐ。

DB 層は「動く」と「本番で耐える」の差が最も大きい領域です。Echo の薄さは、この層を Go の標準的な作法で堅く作れることの裏返しでもあります。次は、この上に載る認証・認可と、全体を束ねるクリーンアーキテクチャへ。

友田

友田 陽大

وزیر (METI) ایوارڈ یافتہ پروڈکٹ کے ڈویلپر۔ TypeScript + Python + AWS کے ساتھ میں SaaS، صنعتی ڈیجیٹل تبدیلی اور پروڈکشن کے قابل جنریٹو اے آئی (RAG) تنہا ابتدا سے انتہا تک فراہم کرتا ہوں — تقاضوں کی تعیین سے انفراسٹرکچر اور آپریشن تک۔

I can take on the implementation from this article as an engagement

I build Go / Echo backends, from design to production

API design and migration to Echo v5, clean architecture (Controller/UseCase/Repository + DI), middleware and security, centralized error handling, graceful shutdown, and testing/CI. With experience building a clean-architecture backend in Go/Echo + google/wire, I implement APIs that don't fall over, are traceable, and are easy to change.

پروجیکٹ کی بنیاد پر کام اور تکنیکی مشاورت دونوں کے لیے دستیاب ہوں۔ 30 منٹ کی مفت مشاورت سے آغاز کریں۔

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

یہ بھی پڑھنے کے قابل