sqlx

sqlxは、Go標準のdatabase/sqlパッケージの強力な拡張ライブラリです。生のSQLとフルORMの中間に位置し、database/sqlのパフォーマンスと柔軟性を維持しながら追加の便利機能を提供します。sqlxは、名前付きパラメータ、構造体スキャン、結果マッピングなどの不足している機能を追加し、SQLクエリの制御を犠牲にすることなく利便性を向上させます。

SQLGoデータベース拡張PostgreSQLMySQLSQLite

GitHub概要

jmoiron/sqlx

general purpose extensions to golang's database/sql

スター17,058
ウォッチ195
フォーク1,108
作成日:2013年1月28日
言語:Go
ライセンス:MIT License

トピックス

なし

スター履歴

jmoiron/sqlx Star History
データ取得日時: 2025/7/19 09:31

ライブラリ

sqlx

概要

sqlxは、Go標準のdatabase/sqlパッケージの強力な拡張ライブラリです。生のSQLとフルORMの中間に位置し、database/sqlのパフォーマンスと柔軟性を維持しながら追加の便利機能を提供します。sqlxは、名前付きパラメータ、構造体スキャン、結果マッピングなどの不足している機能を追加し、SQLクエリの制御を犠牲にすることなく利便性を向上させます。

詳細

sqlxは、標準のdatabase/sqlパッケージを、ボイラープレートコードを削減する機能で強化しながら、SQLクエリを直接記述して最適化する能力を保持します。database/sqlが提供する以上の利便性を求めながら、フルORMのオーバーヘッドと抽象化を望まない開発者に特に好まれています。

主な機能

  • 構造体スキャン: クエリ結果を構造体に自動的にスキャン
  • 名前付きパラメータ: クエリで名前付きパラメータを使用して可読性を向上
  • GetとSelect: 単一行と複数行のクエリのための便利なメソッド
  • INクエリ: スライス展開によるIN句の組み込みサポート
  • Rebind: 異なるSQL方言に対する自動クエリ再バインド
  • トランザクションサポート: 同じ便利なメソッドで強化されたトランザクション処理

長所と短所

長所

  • database/sqlに慣れた開発者にとって学習曲線が最小限
  • パフォーマンス最適化機能を備えたSQLクエリの完全な制御
  • フルORMと比較して低いオーバーヘッド
  • 複雑なクエリやレポート作成に優れている
  • 既存のdatabase/sqlコードと互換性がある
  • 複数のデータベースドライバをサポート

短所

  • 自動スキーマ生成やマイグレーション機能がない
  • すべてのクエリに対して手動でSQLを記述する必要がある
  • 組み込みのリレーションシップ処理がない
  • フルORMと比較してラピッドプロトタイピングには適さない
  • 効果的な使用にはSQLの知識が必要

参考ページ

コード例

基本的なセットアップ

# プロジェクトの初期化
go mod init sqlx-example
go get github.com/jmoiron/sqlx
go get github.com/lib/pq          # PostgreSQLドライバ
go get github.com/go-sql-driver/mysql # MySQLドライバ
go get github.com/mattn/go-sqlite3    # SQLiteドライバ
package main

import (
    "log"
    "github.com/jmoiron/sqlx"
    _ "github.com/lib/pq" // PostgreSQLドライバ
)

// データベーススキーマに一致する構造体を定義
type User struct {
    ID        int    `db:"id"`
    Name      string `db:"name"`
    Email     string `db:"email"`
    Age       int    `db:"age"`
    CreatedAt string `db:"created_at"`
}

type Post struct {
    ID        int    `db:"id"`
    UserID    int    `db:"user_id"`
    Title     string `db:"title"`
    Content   string `db:"content"`
    CreatedAt string `db:"created_at"`
}

func main() {
    // データベースに接続
    db, err := sqlx.Connect("postgres", "user=foo dbname=bar sslmode=disable")
    if err != nil {
        log.Fatalln(err)
    }
    defer db.Close()

    // テーブルを作成(スキーマは手動で管理する必要があります)
    schema := `
    CREATE TABLE IF NOT EXISTS users (
        id SERIAL PRIMARY KEY,
        name VARCHAR(100) NOT NULL,
        email VARCHAR(100) UNIQUE NOT NULL,
        age INTEGER,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
    
    CREATE TABLE IF NOT EXISTS posts (
        id SERIAL PRIMARY KEY,
        user_id INTEGER REFERENCES users(id),
        title VARCHAR(200) NOT NULL,
        content TEXT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );`
    
    db.MustExec(schema)
}

基本的な操作(CRUD)

// Create - ユーザーを挿入
user := User{
    Name:  "John Doe",
    Email: "[email protected]",
    Age:   30,
}

// 名前付きパラメータを使用
result, err := db.NamedExec(`
    INSERT INTO users (name, email, age) 
    VALUES (:name, :email, :age)`, user)
if err != nil {
    log.Fatal(err)
}
id, _ := result.LastInsertId()
fmt.Printf("作成されたユーザーID: %d\n", id)

// 代替案: 位置パラメータを使用
_, err = db.Exec(`
    INSERT INTO users (name, email, age) 
    VALUES ($1, $2, $3)`, "Jane Smith", "[email protected]", 25)

// Read - 単一のユーザーを取得
var user User
err = db.Get(&user, "SELECT * FROM users WHERE id = $1", 1)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("ユーザー: %+v\n", user)

// 複数のユーザーを取得
var users []User
err = db.Select(&users, "SELECT * FROM users WHERE age > $1", 20)
if err != nil {
    log.Fatal(err)
}
for _, u := range users {
    fmt.Printf("ユーザー: %+v\n", u)
}

// Update - ユーザーを更新
_, err = db.Exec(`
    UPDATE users SET age = $1 WHERE id = $2`, 31, 1)
if err != nil {
    log.Fatal(err)
}

// 名前付きパラメータを使用した更新
user.Age = 32
_, err = db.NamedExec(`
    UPDATE users SET age = :age WHERE id = :id`, user)

// Delete - ユーザーを削除
_, err = db.Exec("DELETE FROM users WHERE id = $1", 1)
if err != nil {
    log.Fatal(err)
}

高度なクエリ

// IN句とクエリ展開の使用
ids := []int{1, 2, 3, 4, 5}
query, args, err := sqlx.In("SELECT * FROM users WHERE id IN (?)", ids)
if err != nil {
    log.Fatal(err)
}
// 特定のデータベース用に再バインド(PostgreSQLは$1、$2などを使用)
query = db.Rebind(query)
var users []User
err = db.Select(&users, query, args...)

// マップを使用した名前付きクエリ
m := map[string]interface{}{
    "name": "John%",
    "age":  25,
}
var users []User
nstmt, err := db.PrepareNamed(`
    SELECT * FROM users 
    WHERE name LIKE :name AND age > :age`)
err = nstmt.Select(&users, m)

// 柔軟な結果処理のためのQueryx
rows, err := db.Queryx("SELECT * FROM users")
for rows.Next() {
    var u User
    err = rows.StructScan(&u)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%+v\n", u)
}

// 既存のdatabase/sqlコードでsqlx.DBメソッドを使用
var name string
row := db.QueryRow("SELECT name FROM users WHERE id = $1", 1)
err = row.Scan(&name)

トランザクション

// トランザクションを開始
tx, err := db.Beginx()
if err != nil {
    log.Fatal(err)
}

// トランザクション内ですべてのsqlxメソッドを使用
var user User
err = tx.Get(&user, "SELECT * FROM users WHERE id = $1", 1)
if err != nil {
    tx.Rollback()
    log.Fatal(err)
}

// トランザクション内で更新
_, err = tx.Exec("UPDATE users SET age = age + 1 WHERE id = $1", user.ID)
if err != nil {
    tx.Rollback()
    log.Fatal(err)
}

// 関連レコードを作成
_, err = tx.Exec(`
    INSERT INTO posts (user_id, title, content) 
    VALUES ($1, $2, $3)`, 
    user.ID, "新しい投稿", "これはトランザクションテストです")
if err != nil {
    tx.Rollback()
    log.Fatal(err)
}

// トランザクションをコミット
err = tx.Commit()
if err != nil {
    log.Fatal(err)
}

// トランザクションヘルパー関数を使用
err = sqlx.Transactional(db, func(tx *sqlx.Tx) error {
    // ここでのすべての操作はトランザクション内
    _, err := tx.Exec("INSERT INTO users (name, email) VALUES ($1, $2)", 
        "Bob Wilson", "[email protected]")
    if err != nil {
        return err // 自動的にロールバックをトリガー
    }
    
    var count int
    err = tx.Get(&count, "SELECT COUNT(*) FROM users")
    if err != nil {
        return err
    }
    
    if count > 100 {
        return fmt.Errorf("ユーザーが多すぎます")
    }
    
    return nil // 自動的にコミット
})

プリペアドステートメントと名前付きクエリ

// プリペアドステートメント
stmt, err := db.Preparex("SELECT * FROM users WHERE age > $1")
if err != nil {
    log.Fatal(err)
}
defer stmt.Close()

var users []User
err = stmt.Select(&users, 25)

// 名前付きプリペアドステートメント
nstmt, err := db.PrepareNamed("SELECT * FROM users WHERE name = :name")
if err != nil {
    log.Fatal(err)
}
defer nstmt.Close()

var user User
err = nstmt.Get(&user, map[string]interface{}{"name": "John Doe"})

// プリペアドステートメントを使用したバッチ操作
stmt, err = db.Preparex("INSERT INTO users (name, email, age) VALUES ($1, $2, $3)")
defer stmt.Close()

users := []User{
    {Name: "User1", Email: "[email protected]", Age: 20},
    {Name: "User2", Email: "[email protected]", Age: 25},
    {Name: "User3", Email: "[email protected]", Age: 30},
}

for _, u := range users {
    _, err = stmt.Exec(u.Name, u.Email, u.Age)
    if err != nil {
        log.Printf("ユーザー %s の挿入に失敗: %v", u.Name, err)
    }
}

NULLとカスタム型の処理

import (
    "database/sql"
    "github.com/jmoiron/sqlx/types"
)

type UserWithNulls struct {
    ID          int            `db:"id"`
    Name        string         `db:"name"`
    Email       string         `db:"email"`
    Age         sql.NullInt64  `db:"age"`        // NULL可能な整数
    Bio         sql.NullString `db:"bio"`        // NULL可能な文字列
    Preferences types.JSONText `db:"preferences"` // JSONカラム
}

// NULL値での挿入
user := UserWithNulls{
    Name:  "John Doe",
    Email: "[email protected]",
    Age:   sql.NullInt64{Valid: false}, // NULL
    Bio:   sql.NullString{String: "開発者", Valid: true},
    Preferences: types.JSONText(`{"theme": "dark", "notifications": true}`),
}

_, err = db.NamedExec(`
    INSERT INTO users (name, email, age, bio, preferences) 
    VALUES (:name, :email, :age, :bio, :preferences)`, user)

// NULLハンドリングを伴うクエリ
var users []UserWithNulls
err = db.Select(&users, "SELECT * FROM users WHERE age IS NULL OR age > 25")
for _, u := range users {
    if u.Age.Valid {
        fmt.Printf("ユーザー %s は %d 歳です\n", u.Name, u.Age.Int64)
    } else {
        fmt.Printf("ユーザー %s の年齢は不明です\n", u.Name)
    }
}

// カスタムスキャナーの実装
type Email string

func (e *Email) Scan(src interface{}) error {
    switch s := src.(type) {
    case string:
        *e = Email(s)
    case []byte:
        *e = Email(s)
    case nil:
        *e = ""
    default:
        return fmt.Errorf("サポートされていない型: %T", src)
    }
    return nil
}

パフォーマンス最適化

// 単一クエリでの一括挿入
users := []User{
    {Name: "User1", Email: "[email protected]", Age: 20},
    {Name: "User2", Email: "[email protected]", Age: 25},
    {Name: "User3", Email: "[email protected]", Age: 30},
}

// 一括挿入クエリの構築
valueStrings := make([]string, 0, len(users))
valueArgs := make([]interface{}, 0, len(users) * 3)
for i, u := range users {
    valueStrings = append(valueStrings, fmt.Sprintf("($%d, $%d, $%d)", 
        i*3+1, i*3+2, i*3+3))
    valueArgs = append(valueArgs, u.Name, u.Email, u.Age)
}

query := fmt.Sprintf("INSERT INTO users (name, email, age) VALUES %s", 
    strings.Join(valueStrings, ","))
_, err = db.Exec(query, valueArgs...)

// コネクションプーリングの設定
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)

// タイムアウト制御のためのコンテキスト付きクエリ
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

var users []User
err = db.SelectContext(ctx, &users, "SELECT * FROM users")
if err != nil {
    if err == context.DeadlineExceeded {
        log.Println("クエリタイムアウト")
    }
}