GoのSQLBuilder「squirrel」を使ってみた

## はじめに

Goでデータベースを扱う際、選択肢は主にdatabase/sqlによる直接記述やGormなどのORMがあります。しかし、database/sqlは文字列でのSQL記述が必要なため型安全性に課題があり、一方でORMは複雑なクエリの実装が困難です。そこで今回は、GoのSQLBuilderライブラリの中でも人気の高い「squirrel」を試してみました。

## squirrelとは

squirrelは、Goで使用できるSQLBuilderライブラリです。主な特徴として以下が挙げられます。一般的なSQLBuilderならではのメリットを活用できます。

- SQLを型安全に構築できる
- メソッドチェーンで直感的に書ける
- WHERE句の条件を動的に組み立てやすい

## 基本的な使い方

### SELECT文

```go
func (ur *userRepository) GetByID(ctx context.Context, id int64) (*User, error) {
	query := sq.
		Select("id", "name", "email", "status", "created_at", "updated_at").
		From("users").
		Where(sq.Eq{"id": id})

	queryString, queryArgs, err := query.ToSql()
	if err != nil {
		return nil, fmt.Errorf("failed to build query: %w", err)
	}

	var user User
	err = ur.db.QueryRowContext(ctx, queryString, queryArgs...).Scan(
		&user.ID,
		&user.Name,
		&user.Email,
		&user.Status,
		&user.CreatedAt,
		&user.UpdatedAt,
	)
	if err == sql.ErrNoRows {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("failed to get user: %w", err)
	}

	return &user, nil
}
```

### INSERT文

```go
func (ur *userRepository) Create(ctx context.Context, user *User) error {
	user.CreatedAt = time.Now()
	user.UpdatedAt = time.Now()

	query := sq.
		Insert("users").
		Columns("name", "email", "status", "created_at", "updated_at").
		Values(user.Name, user.Email, user.Status, user.CreatedAt, user.UpdatedAt).
		Suffix("RETURNING id")

	queryString, queryArgs, err := query.ToSql()
	if err != nil {
		return fmt.Errorf("failed to build query: %w", err)
	}

	err = ur.db.QueryRowContext(ctx, queryString, queryArgs...).Scan(&user.ID)
	if err != nil {
		return fmt.Errorf("failed to create user: %w", err)
	}

	return nil
}
```

### UPDATE文

```go
func (ur *userRepository) Update(ctx context.Context, user *User) error {
	user.UpdatedAt = time.Now()

	query := sq.
		Update("users").
		Set("name", user.Name).
		Set("email", user.Email).
		Set("status", user.Status).
		Set("updated_at", user.UpdatedAt).
		Where(sq.Eq{"id": user.ID})

	queryString, queryArgs, err := query.ToSql()
	if err != nil {
		return fmt.Errorf("failed to build query: %w", err)
	}

	result, err := ur.db.ExecContext(ctx, queryString, queryArgs...)
	if err != nil {
		return fmt.Errorf("failed to update user: %w", err)
	}

	affected, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("failed to get affected rows: %w", err)
	}
	if affected == 0 {
		return fmt.Errorf("user not found: %d", user.ID)
	}

	return nil
}
```

### DELETE文

```go:
func (ur *userRepository) Delete(ctx context.Context, id int64) error {
	query := sq.
		Delete("users").
		Where(sq.Eq{"id": id})

	queryString, queryArgs, err := query.ToSql()
	if err != nil {
		return fmt.Errorf("failed to build query: %w", err)
	}

	result, err := ur.db.ExecContext(ctx, queryString, queryArgs...)
	if err != nil {
		return fmt.Errorf("failed to delete user: %w", err)
	}

	affected, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("failed to get affected rows: %w", err)
	}
	if affected == 0 {
		return fmt.Errorf("user not found: %d", id)
	}

	return nil
}
```

## 動的なクエリ構築

squirrelの強みの一つは、条件に応じて動的にクエリを構築できる点です

```go
func (ur *userRepository) Search(ctx context.Context, params SearchParams) ([]User, error) {
    query := sq.
        Select("id", "name", "email", "status", "created_at", "updated_at").
        From("users")

    // 検索条件を動的に追加
    if params.Name != "" {
        query = query.Where(sq.Like{"name": "%" + params.Name + "%"})
    }
    
    if params.Email != "" {
        query = query.Where(sq.Eq{"email": params.Email})
    }
    
    if params.Status != "" {
        query = query.Where(sq.Eq{"status": params.Status})
    }

    // 日付範囲検索
    if !params.CreatedAfter.IsZero() {
        query = query.Where(sq.GtOrEq{"created_at": params.CreatedAfter})
    }
    
    if !params.CreatedBefore.IsZero() {
        query = query.Where(sq.LtOrEq{"created_at": params.CreatedBefore})
    }

    // ソート条件の追加
    if params.OrderBy != "" {
        if params.OrderDesc {
            query = query.OrderBy(params.OrderBy + " DESC")
        } else {
            query = query.OrderBy(params.OrderBy + " ASC")
        }
    }

    // ページネーション
    if params.Limit > 0 {
        query = query.Limit(uint64(params.Limit))
    }
    if params.Offset > 0 {
        query = query.Offset(uint64(params.Offset))
    }

    queryString, queryArgs, err := query.ToSql()
    if err != nil {
        return nil, fmt.Errorf("failed to build query: %w", err)
    }

    // 以降は省略
    rows, err := ur.db.QueryContext(ctx, queryString, queryArgs...)
    // ...
}
```

## まとめ

squirrelを使用してみると、database/sqlと比較して型安全なSQLの構築が可能で、非常に良かったです。Gormのような強力なORMと比べるとコードは冗長になりますが、ユースケースに応じて使い分けることでメリットを最大限に活かせそうです！

## 参考

https://github.com/Masterminds/squirrel