doc.dev1x.org

動的SQLクエリ生成に関する設計パターン

1. 目的

2. 課題

3. 解決策

3-1. 基本構造:Builderパターン + Fluent Interface

type QueryBuilder struct {
    builder sq.SelectBuilder
    joins   map[string]struct{}  // JOIN管理用Set
}

func NewQueryBuilder() *QueryBuilder {
    return &QueryBuilder{
        builder: sq.Select("u.*").From("users u"),
        joins:   make(map[string]struct{}),
    }
}

// 各条件を独立したメソッドに分離
func (b *QueryBuilder) WithUserID(userID int) *QueryBuilder {
    if userID <= 0 {
        return b
    }
    b.builder = b.builder.Where(sq.Eq{"user_id": userID})
    return b
}

3-2. 動的JOIN管理

// JOINが必要な場合のみ追加
func (b *QueryBuilder) ensureOrdersJoin() *QueryBuilder {
    if _, exists := b.joins["orders"]; exists {
        return b // 既にJOIN済み
    }
    b.builder = b.builder.LeftJoin("orders o ON u.id = o.user_id")
    b.joins["orders"] = struct{}{}
    return b
}

// 条件メソッドが必要なJOINを自動追加
func (b *QueryBuilder) WithOrderStatus(status string) *QueryBuilder {
    if status == "" {
        return b
    }
    b.ensureOrdersJoin()  // 自動的にJOIN
    b.builder = b.builder.Where(sq.Eq{"o.status": status})
    return b
}

3-3. JOIN管理のデータ構造選択

Option A: map[string]struct{}(推奨)

joins map[string]struct{}  // メモリ効率的なSet

Option B: []string + slices.Contains

joins []string  // JOIN順序も保持
if slices.Contains(b.joins, "orders") { ... }

Option C: カスタムSet型

type StringSet map[string]struct{}
func (s StringSet) Add(v string) { s[v] = struct{}{} }
func (s StringSet) Contains(v string) bool { _, ok := s[v]; return ok }

Option D: OrderedSet(順序も必要な場合)

type OrderedSet struct {
    items []string
    set   map[string]struct{}
}

3-4. 使用例

func main() {
    params := RequestParams{
        UserID:      100,
        OrderStatus: "pending",
        ProfileBio:  "engineer",
    }
    
    qb := NewQueryBuilder().
        WithUserID(params.UserID).
        WithOrderStatus(params.OrderStatus).   // ordersテーブルを自動JOIN
        WithProfileBio(params.ProfileBio)      // profilesテーブルを自動JOIN
    
    query, args, _ := qb.Build()
    result := dbcon.Query(query, args...)
}

4. メリット

設計面

パフォーマンス面

保守性

5. デメリット

実装コスト

複雑性

パフォーマンス

その他

6. 注意事項

SQLインジェクション対策

// ❌ 危険:文字列連結
b.builder = "SELECT * FROM users WHERE name = '" + name + "'"

// ✅ 安全:プレースホルダー使用
b.builder = b.builder.Where(sq.Eq{"name": name})

JOIN順序と依存関係

// 依存関係があるJOINは順序を意識
func (b *QueryBuilder) ensureOrderItemsJoin() *QueryBuilder {
    b.ensureOrdersJoin()  // 先にordersをJOIN
    if _, exists := b.joins["order_items"]; exists {
        return b
    }
    b.builder = b.builder.LeftJoin("order_items oi ON o.id = oi.order_id")
    b.joins["order_items"] = struct{}{}
    return b
}

データ構造の選択基準

メソッド命名規則

// 推奨:意図が明確な命名
WithUserID()        // 条件を追加
ensureOrdersJoin()  // JOINを保証(内部用)

エラーハンドリング

func (b *QueryBuilder) Build() (string, []interface{}, error) {
    return b.builder.ToSql()  // squirrelのエラーをそのまま返す
}

// 使用側
query, args, err := qb.Build()
if err != nil {
    return fmt.Errorf("failed to build query: %w", err)
}

パフォーマンスチューニング

// 頻繁に使う組み合わせは事前定義も検討
func NewOrderQueryBuilder() *QueryBuilder {
    qb := NewQueryBuilder()
    qb.ensureOrdersJoin()  // 最初からJOIN
    return qb
}

デバッグ支援

// デバッグ用メソッドを用意
func (b *QueryBuilder) Debug() {
    sql, args, _ := b.builder.ToSql()
    fmt.Printf("SQL: %s\nArgs: %v\nJoins: %v\n", sql, args, b.joins)
}

テストの考慮

// テスト用:生成されたSQLを検証
func TestQueryBuilder_WithOrderStatus(t *testing.T) {
    qb := NewQueryBuilder().WithOrderStatus("pending")
    sql, args, _ := qb.Build()
    
    assert.Contains(t, sql, "LEFT JOIN orders")
    assert.Contains(t, sql, "o.status = ?")
    assert.Equal(t, []interface{}{"pending"}, args)
}