メインコンテンツへスキップ
友田 陽大
Go・Echo 本番運用
Go
Echo
PostgreSQL
型安全
アーキテクチャ設計
セキュリティ

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

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

公開日
読了時間
10分
著者
友田 陽大
シェア

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 の標準的な作法で堅く作れることの裏返しでもあります。次は、この上に載る認証・認可と、全体を束ねるクリーンアーキテクチャへ。

友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・ 実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

この記事の実装を、案件として承ります

Go / Echo のバックエンドを、設計から本番運用まで承ります

Echo v5 へのAPI設計・移行、クリーンアーキテクチャ(Controller/UseCase/Repository + DI)、ミドルウェアとセキュリティ、集中エラー処理、グレースフルシャットダウン、テストとCIまで。Go/Echo + google/wire で実際にクリーンアーキのバックエンドを構築した知見で、落ちない・追える・変更しやすいAPIを実装します。

プロジェクト単位(請負)・技術顧問のどちらにも対応可能です。まずは30分の無料技術相談から。

あわせて読みたい