Go での例を含むマルチテナントデータベースパターン

マルチテナントデータベースパターンの完全ガイド

目次

マルチテナント は、SaaS アプリケーションのための基本的なアーキテクチャパターンであり、複数の顧客(テナント)が同じアプリケーションインフラストラクチャを共有しながらも、データの分離を維持することが可能です。

適切なデータベースパターンの選択は、スケーラビリティ、セキュリティ、運用効率にとって非常に重要です。

databases-scheme

マルチテナントパターンの概要

マルチテナントアプリケーションを設計する際には、以下の3つの主なデータベースアーキテクチャパターンから選択することが可能です:

  1. 共有データベース、共有スキーマ(最も一般的)
  2. 共有データベース、個別スキーマ
  3. テナントごとに個別のデータベース

それぞれのパターンには、特徴やトレードオフ、使用ケースが異なります。それぞれを詳しく見ていきましょう。

パターン1:共有データベース、共有スキーマ

これは最も一般的なマルチテナントパターンで、すべてのテナントが同じデータベースとスキーマを共有し、tenant_id列を使用してテナントデータを区別します。

アーキテクチャ

┌─────────────────────────────────────┐
│     Single Database                 │
│  ┌───────────────────────────────┐  │
│  │  Shared Schema                │  │
│  │  - users (tenant_id, ...)     │  │
│  │  - orders (tenant_id, ...)    │  │
│  │  - products (tenant_id, ...)  │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

実装例

マルチテナントパターンを実装する際には、SQLの基本知識が非常に重要です。SQLコマンドや構文に関する包括的なリファレンスが必要な場合は、SQLチートシート をご参照ください。共有スキーマパターンを設定する方法は以下の通りです:

-- tenant_idを含むusersテーブル
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    tenant_id INTEGER NOT NULL,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255),
    created_at TIMESTAMP DEFAULT NOW(),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);

-- パフォーマンス向上のためtenant_idにインデックスを設定
CREATE INDEX idx_users_tenant_id ON users(tenant_id);

-- 行レベルセキュリティ(PostgreSQLの例)
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON users
    FOR ALL
    USING (tenant_id = current_setting('app.current_tenant')::INTEGER);

PostgreSQLに特化した機能やコマンド、RLSポリシー、スキーマ管理、パフォーマンスチューニングについては、PostgreSQLチートシート をご参照ください。

アプリケーションレベルのフィルタリング

Goアプリケーションを使用する際には、適切なORMの選択がマルチテナント実装に大きな影響を与えます。以下の例ではGORMを使用していますが、他にもいくつか優れた選択肢があります。GORM、Ent、Bun、sqlcを含むGo ORMの詳細な比較については、PostgreSQL用Go ORMの包括的なガイド をご参照ください。

// GORMを使用したGoでの例
func GetUserByEmail(db *gorm.DB, tenantID uint, email string) (*User, error) {
    var user User
    err := db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user).Error
    return &user, err
}

// テナントコンテキストを設定するミドルウェア
func TenantMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tenantID := extractTenantID(r) // サブドメイン、ヘッダー、JWTから取得
        ctx := context.WithValue(r.Context(), "tenant_id", tenantID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

共有スキーマの利点

  • コストが最も低い:単一のデータベースインスタンス、インフラストラクチャが最小限
  • 運用が最も簡単:バックアップ、モニタリング、メンテナンスが1つのデータベースで可能
  • スキーマ変更が簡単:すべてのテナントに一度にマイグレーションを適用
  • テナント数が多い場合に最適:リソースの効率的な利用
  • テナント間の分析が容易:テナント間でデータを集計しやすい

共有スキーマの欠点

  • 分離が弱い:tenant_idフィルタを忘れるとデータリークのリスク
  • ノイズの多い隣人:1つのテナントの重いワークロードが他のテナントに影響
  • カスタマイズが限られる:すべてのテナントが同じスキーマを共有
  • コンプライアンスの課題:厳しいデータ分離要件を満たすのが難しい
  • バックアップの複雑さ:個々のテナントデータを簡単に復元できない

共有スキーマが最適なケース

  • 複数の中小テナントを持つSaaSアプリケーション
  • テナントがカスタムスキーマを必要としないアプリケーション
  • コストに敏感なスタートアップ
  • テナント数が多い(数千以上)

パターン2:共有データベース、個別スキーマ

各テナントは同じデータベース内に独自のスキーマを取得し、インフラストラクチャを共有しながらも、より良い分離を提供します。

個別スキーマのアーキテクチャ

┌─────────────────────────────────────┐
│     Single Database                 │
│  ┌──────────┐  ┌──────────┐         │
│  │ Schema A │  │ Schema B │  ...    │
│  │ (Tenant1)│  │ (Tenant2)│         │
│  └──────────┘  └──────────┘         │
└─────────────────────────────────────┘

個別スキーマの実装

PostgreSQLのスキーマはマルチテナントに非常に強力な機能です。PostgreSQLのスキーマ管理、接続文字列、データベース管理コマンドに関する詳細な情報が必要な場合は、PostgreSQLチートシート をご参照ください。

-- テナント用のスキーマを作成
CREATE SCHEMA tenant_123;

-- テナント操作用の検索パスを設定
SET search_path TO tenant_123, public;

-- テナントスキーマ内にテーブルを作成
CREATE TABLE tenant_123.users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255),
    created_at TIMESTAMP DEFAULT NOW()
);

データベース接続管理

マルチテナントアプリケーションでは、データベース接続を効率的に管理することが非常に重要です。以下の接続管理コードはGORMを使用していますが、他のORMオプションも検討する価値があります。Go ORMの比較(接続プール、パフォーマンス特性、使用ケースなど)については、Go ORM比較ガイド をご参照ください。

// スキーマ検索パス付きの接続文字列
func GetTenantDB(tenantID uint) *gorm.DB {
    db := initializeDB()
    db.Exec(fmt.Sprintf("SET search_path TO tenant_%d, public", tenantID))
    return db
}

// またはPostgreSQL接続文字列を使用
// postgresql://user:pass@host/db?search_path=tenant_123

個別スキーマの利点

  • 分離がより良い:スキーマレベルの分離によりデータリークのリスクが減少
  • カスタマイズが可能:各テナントが異なるテーブル構造を持つことができる
  • コストが中程度:まだ単一のデータベースインスタンス
  • テナントごとのバックアップが容易:個々のスキーマをバックアップ可能
  • コンプライアンスに適している:共有スキーマパターンより強力

個別スキーマの欠点

  • スキーマ管理の複雑さ:マイグレーションはテナントごとに実行する必要がある
  • 接続のオーバーヘッド:接続ごとにsearch_pathを設定する必要がある
  • スケーラビリティの限界:スキーマ数の制限(PostgreSQLでは約10,000スキーマ)
  • テナント間のクエリが複雑:動的なスキーマ参照が必要
  • リソースの制限:まだ共有データベースリソース

個別スキーマが最適なケース

  • 中規模のSaaS(数十から数百のテナント)
  • テナントがスキーマカスタマイズを必要とする場合
  • 共有スキーマよりより良い分離が必要なアプリケーション
  • コンプライアンス要件が中程度の場合

パターン3:テナントごとに個別のデータベース

各テナントは独自の完全なデータベースインスタンスを取得し、最大の分離を提供します。

個別データベースのアーキテクチャ

┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│  Database 1  │  │  Database 2  │  │  Database 3  │
│  (Tenant A)  │  │  (Tenant B)  │  │  (Tenant C)  │
└──────────────┘  └──────────────┘  └──────────────┘

個別データベースの実装

-- テナント用のデータベースを作成
CREATE DATABASE tenant_enterprise_corp;

-- テナントデータベースに接続
\c tenant_enterprise_corp

-- テーブルを作成(tenant_idは不要!)
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255),
    created_at TIMESTAMP DEFAULT NOW()
);

動的な接続管理

// 接続プールマネージャ
type TenantDBManager struct {
    pools map[uint]*gorm.DB
    mu    sync.RWMutex
}

func (m *TenantDBManager) GetDB(tenantID uint) (*gorm.DB, error) {
    m.mu.RLock()
    if db, exists := m.pools[tenantID]; exists {
        m.mu.RUnlock()
        return db, nil
    }
    m.mu.RUnlock()

    m.mu.Lock()
    defer m.mu.Unlock()

    // ライターロックを取得した後に再度確認
    if db, exists := m.pools[tenantID]; exists {
        return db, nil
    }

    // 新しい接続を作成
    db, err := gorm.Open(postgres.Open(fmt.Sprintf(
        "host=localhost user=dbuser password=dbpass dbname=tenant_%d sslmode=disable",
        tenantID,
    )), &gorm.Config{})
    
    if err != nil {
        return nil, err
    }

    m.pools[tenantID] = db
    return db, nil
}

個別データベースの利点

  • 最大の分離:完全なデータ分離
  • 最高のセキュリティ:テナント間のデータアクセスのリスクなし
  • 完全なカスタマイズ:各テナントが完全に異なるスキーマを持つことができる
  • 独立したスケーリング:テナントデータベースを個別にスケーリング可能
  • コンプライアンスが容易:最も厳しいデータ分離要件を満たす
  • テナントごとのバックアップ:簡単な独立したバックアップ/リストア
  • ノイズの多い隣人がいない:テナントのワークロードが他のテナントに影響しない

個別データベースの欠点

  • コストが最も高い:複数のデータベースインスタンスが必要でリソースが増える
  • 運用の複雑さ:多くのデータベース(バックアップ、モニタリング、マイグレーション)を管理する必要がある
  • 接続の制限:各データベースインスタンスには接続の制限がある
  • テナント間の分析:データフェデレーションまたはETLが必要
  • マイグレーションの複雑さ:すべてのデータベースにマイグレーションを実行する必要がある
  • リソースのオーバーヘッド:より多くのメモリ、CPU、ストレージが必要

個別データベースが最適なケース

  • 高価値顧客を持つ企業向けSaaS
  • 厳格なコンプライアンス要件(HIPAA、GDPR、SOC 2)
  • テナントが大きなカスタマイズを必要とする場合
  • テナント数が少から中程度(数十から数百)
  • テナントが非常に異なるデータモデルを持つ場合

セキュリティの考慮

選択したパターンに関係なく、セキュリティは最も重要です:

1. 行レベルセキュリティ(RLS)

PostgreSQLのRLSは、クエリをテナントごとに自動的にフィルタリングし、データベースレベルのセキュリティレイヤーを提供します。この機能は特にマルチテナントアプリケーションにおいて非常に強力です。PostgreSQL RLS、セキュリティポリシー、その他の高度なPostgreSQL機能に関する詳細については、PostgreSQLチートシート をご参照ください。

-- RLSを有効にする
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- テナントごとの分離ポリシーを作成
CREATE POLICY tenant_isolation ON orders
    FOR ALL
    USING (tenant_id = current_setting('app.current_tenant')::INTEGER);

-- アプリケーションでテナントコンテキストを設定
SET app.current_tenant = '123';

2. アプリケーションレベルのフィルタリング

アプリケーションコードで常にtenant_idでフィルタリングしてください。以下の例ではGORMを使用していますが、他のORMにはそれぞれクエリ構築の独自のアプローチがあります。マルチテナントアプリケーションに適したORMの選択に関するガイドラインについては、Go ORM比較 をご参照ください。

// ❌ BAD - tenantフィルタが欠如
db.Where("email = ?", email).First(&user)

// ✅ GOOD - tenantフィルタを常に含める
db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user)

// ✅ BETTER - スコープやミドルウェアを使用
db.Scopes(TenantScope(tenantID)).Where("email = ?", email).First(&user)

3. 接続プール

テナントコンテキストをサポートする接続プーラーを使用してください:

// PgBouncerを使用したトランザクションプール
// またはアプリケーションレベルの接続ルーティングを使用

4. オーディットログ

すべてのテナントデータアクセスを記録してください:

type AuditLog struct {
    ID        uint
    TenantID  uint
    UserID    uint
    Action    string
    Table     string
    RecordID  uint
    Timestamp time.Time
    IPAddress string
}

パフォーマンス最適化

インデックス戦略

マルチテナントデータベースのパフォーマンスには適切なインデックスが非常に重要です。複合インデックスや部分インデックスを含むSQLインデックス戦略の理解は不可欠です。CREATE INDEXやクエリ最適化に関するSQLコマンドの包括的なリファレンスが必要な場合は、SQLチートシート をご参照ください。PostgreSQLに特化したインデックス機能やパフォーマンスチューニングについては、PostgreSQLチートシート をご参照ください。

-- テナントクエリ用の複合インデックス
CREATE INDEX idx_orders_tenant_created ON orders(tenant_id, created_at DESC);
CREATE INDEX idx_orders_tenant_status ON orders(tenant_id, status);

-- 一般的なテナント固有クエリ用の部分インデックス
CREATE INDEX idx_orders_active_tenant ON orders(tenant_id, created_at)
WHERE status = 'active';

クエリ最適化

// テナントクエリ用に準備されたステートメントを使用
stmt := db.Prepare("SELECT * FROM users WHERE tenant_id = $1 AND email = $2")

// テナントごとにバッチ操作
db.Where("tenant_id = ?", tenantID).Find(&users)

// 個別データベースパターン用にテナントごとの接続プールを使用

モニタリング

マルチテナントアプリケーションのモニタリングには、効果的なデータベース管理ツールが不可欠です。すべてのテナントにわたってクエリパフォーマンス、リソース使用量、データベースの健康状態を追跡する必要があります。この目的で役立つデータベース管理ツールの比較については、DBeaver vs Beekeeper比較 をご参照ください。両方のツールは、マルチテナント環境でのPostgreSQLデータベースの管理とモニタリングに優れた機能を提供しています。

テナントごとのメトリクスをモニタリングしてください:

  • テナントごとのクエリパフォーマンス
  • テナントごとのリソース使用量
  • テナントごとの接続数
  • テナントごとのデータベースサイズ

マイグレーション戦略

共有スキーマパターン

データベースマイグレーションを実装する際、選択したORMはスキーマ変更の取り扱いに影響を与えます。以下の例ではGORMのAutoMigrate機能を使用していますが、他のORMには異なるマイグレーション戦略があります。さまざまなGo ORMがマイグレーションやスキーマ管理をどのように処理するかについては、Go ORM比較 をご参照ください。

// マイグレーションはすべてのテナントに自動的に適用
func Migrate(db *gorm.DB) error {
    return db.AutoMigrate(&User{}, &Order{}, &Product{})
}

個別スキーマ/データベースパターン

// テナントごとにマイグレーションを実行
func MigrateAllTenants(tenantIDs []uint) error {
    for _, tenantID := range tenantIDs {
        db := GetTenantDB(tenantID)
        if err := db.AutoMigrate(&User{}, &Order{}); err != nil {
            return fmt.Errorf("tenant %d: %w", tenantID, err)
        }
    }
    return nil
}

決定マトリクス

要因 共有スキーマ 個別スキーマ 個別データベース
分離
コスト
スケーラビリティ 低-中
カスタマイズ 無し
運用の複雑さ
コンプライアンス 限定的 良好 優れた
最適なテナント数 1000+ 10-1000 1-100

ハイブリッドアプローチ

異なるテナント階層にパターンを組み合わせることができます:

// 小規模テナント:共有スキーマ
if tenant.Tier == "standard" {
    return GetSharedDB(tenant.ID)
}

// エンタープライズテナント:個別データベース
if tenant.Tier == "enterprise" {
    return GetTenantDB(tenant.ID)
}

ベストプラクティス

  1. 常にテナントでフィルタリング:アプリケーションコードだけに頼らず、RLSを使用可能であれば使用してください。SQLの基本知識は適切なクエリ構築を保証します。クエリのベストプラクティスについては、SQLチートシート をご参照ください。
  2. テナントリソース使用量をモニタリング:ノイズの多い隣人を特定し、制限してください。DBeaver vs Beekeeperガイド に記載されているデータベース管理ツールを使用してパフォーマンスメトリクスを追跡してください。
  3. テナントコンテキストミドルウェアを実装:テナントの抽出と検証を中央集約してください。ORMの選択はこの実装方法に影響を与えます。Go ORM比較 をご参照ください。
  4. 接続プールを使用:データベース接続を効率的に管理してください。PostgreSQLに特化した接続プール戦略については、PostgreSQLチートシート をご参照ください。
  5. テナントのマイグレーションを計画:テナントをパターン間で移動できるようにする
  6. ソフト削除を実装:テナントデータのハード削除ではなく、deleted_atを使用してください
  7. すべてを監査:コンプライアンスのためにすべてのテナントデータアクセスをログに記録してください
  8. 分離のテスト:クロステナントデータリークを防ぐために定期的なセキュリティ監査を実施してください

結論

適切なマルチテナントデータベースパターンの選択は、分離、コスト、スケーラビリティ、運用の複雑さの要件に大きく依存します。共有データベース、共有スキーマパターンは、ほとんどのSaaSアプリケーションに適していますが、厳格なコンプライアンス要件を持つエンタープライズ顧客には、テナントごとに個別のデータベースが必要です。

最初に要件を満たす最も単純なパターンから始め、要件が進化するにつれてより分離されたパターンへの移行を計画してください。選択したパターンに関係なく、セキュリティとデータ分離を常に最優先事項としてください。

有用なリンク