Queries

Bob のクエリ生成機能は、手書きの SQL クエリから型安全な Go コードを生成します。

基本的なクエリ生成

SQL ファイルの作成

queries/users.sql ファイルを作成:

-- name: GetUser :one
SELECT id, name, email, created_at
FROM users
WHERE id = $1;

-- name: ListUsers :many
SELECT id, name, email, created_at
FROM users
ORDER BY created_at DESC;

-- name: CreateUser :one
INSERT INTO users (name, email)
VALUES ($1, $2)
RETURNING id, name, email, created_at;

-- name: UpdateUser :exec
UPDATE users
SET name = $1, email = $2
WHERE id = $3;

-- name: DeleteUser :exec
DELETE FROM users
WHERE id = $1;

生成されるコード

// User represents a user from the database
type User struct {
    ID        int       `db:"id"`
    Name      string    `db:"name"`
    Email     string    `db:"email"`
    CreatedAt time.Time `db:"created_at"`
}

// GetUserParams contains the parameters for the GetUser query
type GetUserParams struct {
    ID int `db:"id"`
}

// GetUser retrieves a single user by ID
func (q *Queries) GetUser(ctx context.Context, id int) (*User, error) {
    query := `SELECT id, name, email, created_at FROM users WHERE id = $1`
    
    var user User
    err := q.db.QueryRowContext(ctx, query, id).Scan(
        &user.ID,
        &user.Name,
        &user.Email,
        &user.CreatedAt,
    )
    
    if err != nil {
        return nil, err
    }
    
    return &user, nil
}

// ListUsers retrieves all users
func (q *Queries) ListUsers(ctx context.Context) ([]User, error) {
    query := `SELECT id, name, email, created_at FROM users ORDER BY created_at DESC`
    
    rows, err := q.db.QueryContext(ctx, query)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    
    var users []User
    for rows.Next() {
        var user User
        err := rows.Scan(
            &user.ID,
            &user.Name,
            &user.Email,
            &user.CreatedAt,
        )
        if err != nil {
            return nil, err
        }
        users = append(users, user)
    }
    
    return users, nil
}

// CreateUserParams contains the parameters for the CreateUser query
type CreateUserParams struct {
    Name  string `db:"name"`
    Email string `db:"email"`
}

// CreateUser creates a new user
func (q *Queries) CreateUser(ctx context.Context, params CreateUserParams) (*User, error) {
    query := `INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email, created_at`
    
    var user User
    err := q.db.QueryRowContext(ctx, query, params.Name, params.Email).Scan(
        &user.ID,
        &user.Name,
        &user.Email,
        &user.CreatedAt,
    )
    
    if err != nil {
        return nil, err
    }
    
    return &user, nil
}

複雑なクエリ

JOIN クエリ

-- name: GetUserWithPosts :many
SELECT 
    u.id as user_id,
    u.name as user_name,
    u.email as user_email,
    p.id as post_id,
    p.title as post_title,
    p.content as post_content,
    p.published as post_published
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
WHERE u.id = $1;

生成されるコード:

type GetUserWithPostsRow struct {
    UserID        int            `db:"user_id"`
    UserName      string         `db:"user_name"`
    UserEmail     string         `db:"user_email"`
    PostID        sql.NullInt64  `db:"post_id"`
    PostTitle     sql.NullString `db:"post_title"`
    PostContent   sql.NullString `db:"post_content"`
    PostPublished sql.NullBool   `db:"post_published"`
}

func (q *Queries) GetUserWithPosts(ctx context.Context, userID int) ([]GetUserWithPostsRow, error) {
    query := `
        SELECT 
            u.id as user_id,
            u.name as user_name,
            u.email as user_email,
            p.id as post_id,
            p.title as post_title,
            p.content as post_content,
            p.published as post_published
        FROM users u
        LEFT JOIN posts p ON u.id = p.user_id
        WHERE u.id = $1
    `
    
    rows, err := q.db.QueryContext(ctx, query, userID)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    
    var result []GetUserWithPostsRow
    for rows.Next() {
        var row GetUserWithPostsRow
        err := rows.Scan(
            &row.UserID,
            &row.UserName,
            &row.UserEmail,
            &row.PostID,
            &row.PostTitle,
            &row.PostContent,
            &row.PostPublished,
        )
        if err != nil {
            return nil, err
        }
        result = append(result, row)
    }
    
    return result, nil
}

集計クエリ

-- name: GetUserStats :one
SELECT 
    COUNT(*) as total_users,
    COUNT(CASE WHEN created_at > NOW() - INTERVAL '30 days' THEN 1 END) as recent_users,
    AVG(EXTRACT(YEAR FROM AGE(created_at))) as avg_age_years
FROM users;

生成されるコード:

type GetUserStatsRow struct {
    TotalUsers   int64          `db:"total_users"`
    RecentUsers  int64          `db:"recent_users"`
    AvgAgeYears  sql.NullFloat64 `db:"avg_age_years"`
}

func (q *Queries) GetUserStats(ctx context.Context) (*GetUserStatsRow, error) {
    query := `
        SELECT 
            COUNT(*) as total_users,
            COUNT(CASE WHEN created_at > NOW() - INTERVAL '30 days' THEN 1 END) as recent_users,
            AVG(EXTRACT(YEAR FROM AGE(created_at))) as avg_age_years
        FROM users
    `
    
    var stats GetUserStatsRow
    err := q.db.QueryRowContext(ctx, query).Scan(
        &stats.TotalUsers,
        &stats.RecentUsers,
        &stats.AvgAgeYears,
    )
    
    if err != nil {
        return nil, err
    }
    
    return &stats, nil
}

動的クエリ

条件付きクエリ

-- name: SearchUsers :many
SELECT id, name, email, created_at
FROM users
WHERE 
    ($1::text IS NULL OR name ILIKE '%' || $1 || '%')
    AND ($2::text IS NULL OR email ILIKE '%' || $2 || '%')
    AND ($3::timestamp IS NULL OR created_at > $3)
ORDER BY created_at DESC
LIMIT $4 OFFSET $5;

生成されるコード:

type SearchUsersParams struct {
    NameFilter      sql.NullString    `db:"name_filter"`
    EmailFilter     sql.NullString    `db:"email_filter"`
    CreatedAfter    sql.NullTime      `db:"created_after"`
    Limit           int               `db:"limit"`
    Offset          int               `db:"offset"`
}

func (q *Queries) SearchUsers(ctx context.Context, params SearchUsersParams) ([]User, error) {
    query := `
        SELECT id, name, email, created_at
        FROM users
        WHERE 
            ($1::text IS NULL OR name ILIKE '%' || $1 || '%')
            AND ($2::text IS NULL OR email ILIKE '%' || $2 || '%')
            AND ($3::timestamp IS NULL OR created_at > $3)
        ORDER BY created_at DESC
        LIMIT $4 OFFSET $5
    `
    
    rows, err := q.db.QueryContext(ctx, query, 
        params.NameFilter,
        params.EmailFilter,
        params.CreatedAfter,
        params.Limit,
        params.Offset,
    )
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    
    var users []User
    for rows.Next() {
        var user User
        err := rows.Scan(
            &user.ID,
            &user.Name,
            &user.Email,
            &user.CreatedAt,
        )
        if err != nil {
            return nil, err
        }
        users = append(users, user)
    }
    
    return users, nil
}

使用例

func main() {
    db, err := sql.Open("postgres", "...")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
    
    queries := New(db)
    ctx := context.Background()
    
    // ユーザーの作成
    user, err := queries.CreateUser(ctx, CreateUserParams{
        Name:  "John Doe",
        Email: "john@example.com",
    })
    if err != nil {
        log.Fatal(err)
    }
    
    // ユーザーの取得
    retrievedUser, err := queries.GetUser(ctx, user.ID)
    if err != nil {
        log.Fatal(err)
    }
    
    // 全ユーザーの取得
    allUsers, err := queries.ListUsers(ctx)
    if err != nil {
        log.Fatal(err)
    }
    
    // 検索
    searchResults, err := queries.SearchUsers(ctx, SearchUsersParams{
        NameFilter:   sql.NullString{String: "John", Valid: true},
        EmailFilter:  sql.NullString{},
        CreatedAfter: sql.NullTime{},
        Limit:        10,
        Offset:       0,
    })
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("Found %d users\n", len(searchResults))
}

設定オプション

queries:
  enabled: true
  package: "queries"
  output: "./db/queries"
  sql_files:
    - "queries/*.sql"
  overrides:
    - column: "users.id"
      go_type: "int64"
    - column: "users.metadata"
      go_type: "json.RawMessage"

クエリタイプ

  • :one - 単一の結果を返す
  • :many - 複数の結果を返す
  • :exec - 実行結果のみを返す
  • :execrows - 実行結果と影響を受けた行数を返す
  • :execlastid - 実行結果と最後の挿入IDを返す

Bob のクエリ生成機能により、手書きの SQL を型安全な Go コードに変換し、パフォーマンスと安全性を両立できます。