sqlx
sqlxは、Go標準のdatabase/sqlパッケージの強力な拡張ライブラリです。生のSQLとフルORMの中間に位置し、database/sqlのパフォーマンスと柔軟性を維持しながら追加の便利機能を提供します。sqlxは、名前付きパラメータ、構造体スキャン、結果マッピングなどの不足している機能を追加し、SQLクエリの制御を犠牲にすることなく利便性を向上させます。
GitHub概要
jmoiron/sqlx
general purpose extensions to golang's database/sql
スター17,058
ウォッチ195
フォーク1,108
作成日:2013年1月28日
言語:Go
ライセンス:MIT License
トピックス
なし
スター履歴
データ取得日時: 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("クエリタイムアウト")
}
}