Go: Goのデータベース操作:database/sql、CRUD、インジェクション防止のためのプリコンパイル済みステート…

Goのdatabase/sqlパッケージは、SQLデータベース操作のための統一されたインターフェースを提供しています。ドライバーを変更するだけで、MySQL、SQLite、PostgreSQLを切り替えることができます。

SQLインジェクションの防止、トランザクションの管理、接続プールの設定など、データベースを安全に扱う必要がある場合、database/sql 標準ライブラリは、完全かつ安全なツール一式を提供します。

1. 学習内容



2. バックエンドエンジニアの実話

(1) 課題:SQLにおける文字列連結が、ハッカーによるインジェクション攻撃を招き、データベースの削除につながった

ボブは、あるECプラットフォームのバックエンドエンジニアです。彼は、商品検索APIを実装する必要があります:

「私はfmt.Sprintfを使って、商品に関するSQLクエリを作成しました:SELECT * FROM products WHERE name LIKE '%" + search + "%'。サービス開始から2日目、誰かが検索ボックスに' OR 1=1; DROP TABLE products;--と入力しました。すると、私の商品テーブルが消えてしまいました。上司から『データベースにあった10万件の商品はどこへ消えたんだ?』と尋ねられました。」

GO
// Bad code: string concatenation SQL, fatal vulnerability
func searchProducts(w http.ResponseWriter, r *http.Request) {
    search := r.URL.Query().Get("q")
    // If search = "' OR 1=1; DROP TABLE products;--"
    // Final SQL: SELECT * FROM products WHERE name LIKE '%' OR 1=1; DROP TABLE products;--%'
    query := fmt.Sprintf("SELECT * FROM products WHERE name LIKE '%%%s%%'", search)
    rows, err := db.Query(query)  // Nightmare begins
}

(2) Goの解決策:プリコンパイル済みのPreparedStatement

GO
// Good code: precompiled, parameters automatically escaped
func searchProducts(w http.ResponseWriter, r *http.Request) {
    search := r.URL.Query().Get("q")

    // ? is a placeholder; the database driver automatically escapes parameters
    // The search value is always treated as a string, never as part of SQL syntax
    stmt, err := db.Prepare("SELECT * FROM products WHERE name LIKE ?")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    defer stmt.Close()

    rows, err := stmt.Query("%" + search + "%")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    defer rows.Close()

    // Process results...
}

(3) パフォーマンス:文字列連結とプリコンパイル済みSQLの比較

次元 文字列の連結 PreparedStatement
SQLインジェクション ❌ 高リスク ✅ 自動エスケープ
パフォーマンス 実行ごとのSQL解析 ✅ 解析は1回のみ。その後の実行では高速化
読みやすさ 分かりにくい ✅ 分かりやすい
パラメータの型 すべて文字列に変換 ✅ 型を維持


3. データベースへの接続

▶ サンプル:SQLiteへの接続

⚙️ 前提条件: go get github.com/mattn/go-sqlite3 を実行する ⚠️ 注意:go-sqlite3 には CGO が必要です。Windows では gcc(MinGW-w64)が必要ですが、macOS/Linux ではデフォルトで含まれています。

GO
package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/mattn/go-sqlite3"  // Import driver (_ means only init is executed, not directly used)
)

func main() {
    // Open database (actual connection is established on first query)
    db, err := sql.Open("sqlite3", "./test.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Test if connection is reachable
    err = db.Ping()
    if err != nil {
        log.Fatal("Cannot connect to database:", err)
    }

    fmt.Println("Database connected successfully")
}
▶ 試してみよう

▶ サンプル:MySQLへの接続

⚙️ 前提条件: go get github.com/go-sql-driver/mysql を実行する

GO
package main

import (
    "database/sql"
    "fmt"
    "log"
    "time"

    _ "github.com/go-sql-driver/mysql"
)

func main() {
    // DSN format: user:password@tcp(host:port)/dbname?params
    dsn := "root:password@tcp(127.0.0.1:3306)/shop?charset=utf8mb4&parseTime=true"

    db, err := sql.Open("mysql", dsn)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Connection pool configuration
    db.SetMaxOpenConns(25)               // Maximum open connections
    db.SetMaxIdleConns(10)               // Maximum idle connections
    db.SetConnMaxLifetime(5 * time.Minute) // Maximum connection lifetime
    db.SetConnMaxIdleTime(2 * time.Minute) // Maximum idle connection lifetime

    if err = db.Ping(); err != nil {
        log.Fatal("Cannot connect to database:", err)
    }
    fmt.Println("MySQL connected successfully")
}
▶ 試してみよう

(3) database/sql パッケージの主要なメソッド

メソッド 目的 戻り値
sql.Open(driver, dsn) データベースを開く(遅延読み込み) *DB, error
db.Ping() 接続が実際に可能かどうかをテストします error
db.Close() データベースを閉じる error
db.Exec(sql, args...) INSERT/UPDATE/DELETE を実行 Result, error
db.Query(sql, args...) SELECT文を実行し、複数の行を返す *Rows, error
db.QueryRow(sql, args...) SELECT文を実行し、1行を返す *Row
db.Prepare(sql) SQLのプリコンパイル *Stmt, error
🔥 よくある間違い: sql.Open は実際には接続を確立するものではなく、単に DSN の形式を検証するだけです。実際の接続は、最初の Ping または Query / Exec の実行時に確立されます。 したがって、sql.Opennil error を返すからといって、データベースにアクセスできるとは限りません。



4. CRUD操作

▶ サンプル:テーブルの作成とデータの挿入

GO
package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/mattn/go-sqlite3"
)

func main() {
    db, err := sql.Open("sqlite3", "./shop.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Create table
    createSQL := `
    CREATE TABLE IF NOT EXISTS products (
        id    INTEGER PRIMARY KEY AUTOINCREMENT,
        name  TEXT NOT NULL,
        price REAL NOT NULL,
        stock INTEGER NOT NULL DEFAULT 0,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )`
    _, err = db.Exec(createSQL)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Table created successfully")

    // Insert data (Exec returns Result)
    result, err := db.Exec(
        "INSERT INTO products (name, price, stock) VALUES (?, ?, ?)",
        "Laptop", 999.99, 10,
    )
    if err != nil {
        log.Fatal(err)
    }

    id, _ := result.LastInsertId()
    affected, _ := result.RowsAffected()
    fmt.Printf("Inserted: id=%d, rows affected=%d\n", id, affected)
}
▶ 試してみよう

▶ サンプル:データの照会

GO 📖 参照専用
package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/mattn/go-sqlite3"
)

type Product struct {
    ID    int
    Name  string
    Price float64
    Stock int
}

func main() {
    db, err := sql.Open("sqlite3", "./shop.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // QueryRow: query a single row
    var p Product
    err = db.QueryRow("SELECT id, name, price, stock FROM products WHERE id = ?", 1).
        Scan(&p.ID, &p.Name, &p.Price, &p.Stock)
    if err == sql.ErrNoRows {
        fmt.Println("Product not found")
    } else if err != nil {
        log.Fatal(err)
    } else {
        fmt.Printf("Product: %+v\n", p)
    }

    // Query: query multiple rows
    rows, err := db.Query("SELECT id, name, price, stock FROM products WHERE price < ?", 500)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    for rows.Next() {
        var pr Product
        err := rows.Scan(&pr.ID, &pr.Name, &pr.Price, &pr.Stock)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("  Product: %+v\n", pr)
    }
    // Check if iteration encountered an error
    if err = rows.Err(); err != nil {
        log.Fatal(err)
    }
}
論理コード 46 行(40 行制限超過、参照専用)

(3) CRUDメソッドの比較

操作 方法 戻り値 ユースケース
作成 db.Exec(INSERT...) Result (LastInsertId + RowsAffected) 挿入/更新/削除
1行を読み込む db.QueryRow(SELECT...).Scan() *Row + 自動閉じ 1行クエリ
複数行の読み取り db.Query(SELECT...); rows.Scan() *Rows(反復処理と閉じ処理が必要) 複数行クエリ
更新 db.Exec(UPDATE...) Result (影響行数) データの更新
削除 db.Exec(DELETE...) Result (影響を受ける行数) データを削除
🔥 よくある間違い: rows.Close() は、たとえ for rows.Next() のすべての行をすでに処理し終えていたとしても、必ず呼び出す必要があります。閉じないと、接続リーク(接続がプールに解放されない)が発生します。defer rows.Close() を使用して、確実に解放されるようにしてください。



5. PreparedStatement

▶ サンプル:プリコンパイルされたバッチ挿入

GO
package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/mattn/go-sqlite3"
)

type Product struct {
    Name  string
    Price float64
    Stock int
}

func main() {
    db, err := sql.Open("sqlite3", "./shop.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Precompile SQL statement
    stmt, err := db.Prepare("INSERT INTO products (name, price, stock) VALUES (?, ?, ?)")
    if err != nil {
        log.Fatal(err)
    }
    defer stmt.Close()

    // Batch insert (SQL compiled only once)
    products := []Product{
        {"Mouse", 29.99, 100},
        {"Keyboard", 79.99, 50},
        {"Monitor", 299.99, 20},
        {"USB-C Hub", 49.99, 200},
    }

    for _, p := range products {
        result, err := stmt.Exec(p.Name, p.Price, p.Stock)
        if err != nil {
            log.Printf("Insert failed %s: %v", p.Name, err)
            continue
        }
        id, _ := result.LastInsertId()
        fmt.Printf("Inserted successfully: %s (id=%d)\n", p.Name, id)
    }
}
▶ 試してみよう

(2) プリコンパイルされたSQLと連結されたSQL

比較 プリコンパイル Prepare SQL連結
SQLインジェクション ✅ パラメータの自動エスケープ ❌ 高リスク
パフォーマンス(複数回の実行) ✅ 解析は1回のみ ❌ 毎回解析
コードの可読性 明確(パラメータのプレースホルダーとして「?」を使用) 分かりにくい(引用符が入れ子になっている)
型安全性 ✅ パラメータの型を維持 ❌ すべて文字列に変換
適用されるシナリオ すべてのユーザー入力 静的SQLのみ(テーブル名・カラム名。ユーザー入力は含まない)


6. 取引

▶ サンプル:取引の振替

GO 📖 参照専用
package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/mattn/go-sqlite3"
)

func transferFunds(db *sql.DB, fromID, toID int, amount float64) error {
    // Begin transaction
    tx, err := db.Begin()
    if err != nil {
        return fmt.Errorf("failed to begin transaction: %w", err)
    }
    // Rollback on transaction failure
    defer tx.Rollback()  // If Commit succeeds, Rollback is a no-op

    // 1. Deduct from fromID
    result, err := tx.Exec(
        "UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?",
        amount, fromID, amount,
    )
    if err != nil {
        return fmt.Errorf("deduction failed: %w", err)
    }
    affected, _ := result.RowsAffected()
    if affected == 0 {
        return fmt.Errorf("insufficient balance or account does not exist")
    }

    // 2. Add to toID
    result, err = tx.Exec(
        "UPDATE accounts SET balance = balance + ? WHERE id = ?",
        amount, toID,
    )
    if err != nil {
        return fmt.Errorf("credit failed: %w", err)
    }
    affected, _ = result.RowsAffected()
    if affected == 0 {
        return fmt.Errorf("recipient account does not exist")
    }

    // Commit transaction
    return tx.Commit()
}

func main() {
    db, err := sql.Open("sqlite3", "./bank.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Initialize accounts
    db.Exec("CREATE TABLE IF NOT EXISTS accounts (id INTEGER PRIMARY KEY, balance REAL)")
    db.Exec("INSERT OR IGNORE INTO accounts VALUES (1, 1000.00)")
    db.Exec("INSERT OR IGNORE INTO accounts VALUES (2, 500.00)")

    // Transfer 200 from account 1 to 2
    err = transferFunds(db, 1, 2, 200)
    if err != nil {
        log.Printf("Transfer failed: %v\n", err)
    } else {
        fmt.Println("Transfer successful!")
    }
}
論理コード 53 行(40 行制限超過、参照専用)
100%
sequenceDiagram
    participant App as Application
    participant DB as Database

    App->>DB: BEGIN
    DB-->>App: OK
    App->>DB: UPDATE SET balance = balance - ? WHERE id = 1
    DB-->>App: 1 row affected
    App->>DB: UPDATE SET balance = balance + ? WHERE id = 2
    DB-->>App: 1 row affected
    App->>DB: COMMIT
    DB-->>App: OK (persisted)
    Note over App,DB: If error midway → ROLLBACK<br/>All changes reverted
💡 ヒント: トランザクション内で defer tx.Rollback() を使用するのは安全なパターンです。もし Commit が成功すれば、Rollback の呼び出しは安全(何もしない操作)です。もし Commit が失敗した場合、defer が自動的にトランザクションをロールバックします。Rollbackを絶対に省略しないでください!



7. コネクションプールの設定

GO
package main

import (
    "database/sql"
    "fmt"
    "time"

    _ "github.com/go-sql-driver/mysql"
)

func configurePool(db *sql.DB) {
    // Maximum open connections (new requests queue when this limit is reached)
    db.SetMaxOpenConns(25)

    // Maximum idle connections (kept open but unused)
    db.SetMaxIdleConns(10)

    // Maximum connection lifetime (prevents long-running connections from being dropped by the database)
    db.SetConnMaxLifetime(5 * time.Minute)

    // Maximum idle connection lifetime
    db.SetConnMaxIdleTime(2 * time.Minute)
}

func main() {
    db, _ := sql.Open("mysql", "user:pass@/dbname")
    configurePool(db)
    fmt.Println("Connection pool configured")
}

(1) 接続プールパラメータ

パラメータ デフォルト値 推奨値 説明
SetMaxOpenConns 0 (無制限) 25–100 同時接続数の上限
SetMaxIdleConns 2 10–25 アイドル接続の最大数(MaxOpenConns以下)
SetConnMaxLifetime 0(有効期限なし) 5~30分 最大接続有効期間
SetConnMaxIdleTime 0(有効期限なし) 2~5分 アイドル時の接続タイムアウト
🔥 よくある間違い: MaxIdleConnsMaxOpenConns より大きく設定することはできません。データベースやSQLが自動的に調整します。また、MaxOpenConns=0 を無制限に設定しないでください。同時アクセス数が多くなると、接続数が膨大になり、データベースに過負荷がかかります。常に妥当な上限値を設定してください。



8. 完全な例:Eコマースの在庫管理

⚙️ 前提条件: go get github.com/mattn/go-sqlite3 を実行する

GO
// inventory.go
package main

import (
    "database/sql"
    "fmt"
    "log"
    "sync"
    "time"

    _ "github.com/mattn/go-sqlite3"
)

// ---------- Store ----------

type InventoryStore struct {
    db *sql.DB
    mu sync.RWMutex
}

func NewInventoryStore(dbPath string) (*InventoryStore, error) {
    db, err := sql.Open("sqlite3", dbPath)
    if err != nil {
        return nil, fmt.Errorf("failed to open database: %w", err)
    }

    // Connection pool configuration
    db.SetMaxOpenConns(10)
    db.SetMaxIdleConns(5)
    db.SetConnMaxLifetime(5 * time.Minute)

    store := &InventoryStore{db: db}
    if err := store.initSchema(); err != nil {
        return nil, fmt.Errorf("failed to initialize schema: %w", err)
    }
    return store, nil
}

func (s *InventoryStore) initSchema() error {
    schema := `
    CREATE TABLE IF NOT EXISTS products (
        id    INTEGER PRIMARY KEY AUTOINCREMENT,
        name  TEXT NOT NULL,
        price REAL NOT NULL,
        stock INTEGER NOT NULL DEFAULT 0,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    );
    CREATE TABLE IF NOT EXISTS orders (
        id         INTEGER PRIMARY KEY AUTOINCREMENT,
        product_id INTEGER NOT NULL,
        quantity   INTEGER NOT NULL,
        total      REAL NOT NULL,
        status     TEXT NOT NULL DEFAULT 'pending',
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (product_id) REFERENCES products(id)
    );`
    _, err := s.db.Exec(schema)
    return err
}

// ---------- Product Operations ----------

func (s *InventoryStore) AddProduct(name string, price float64, stock int) (int64, error) {
    result, err := s.db.Exec(
        "INSERT INTO products (name, price, stock) VALUES (?, ?, ?)",
        name, price, stock,
    )
    if err != nil {
        return 0, err
    }
    return result.LastInsertId()
}

func (s *InventoryStore) GetProduct(id int) (product, error) {
    var p product
    err := s.db.QueryRow(
        "SELECT id, name, price, stock, created_at FROM products WHERE id = ?", id,
    ).Scan(&p.ID, &p.Name, &p.Price, &p.Stock, &p.CreatedAt)
    if err == sql.ErrNoRows {
        return p, fmt.Errorf("product not found")
    }
    return p, err
}

type product struct {
    ID        int
    Name      string
    Price     float64
    Stock     int
    CreatedAt time.Time
}

// ---------- Order Operations (transaction) ----------

type OrderRequest struct {
    ProductID int
    Quantity  int
}

func (s *InventoryStore) PlaceOrder(req OrderRequest) (int64, error) {
    s.mu.Lock()         // Prevent overselling: only one order deduction at a time
    defer s.mu.Unlock()

    tx, err := s.db.Begin()
    if err != nil {
        return 0, fmt.Errorf("failed to begin transaction: %w", err)
    }
    defer tx.Rollback() // Safe rollback

    // 1. Query product and lock row (⚠️ SQLite does not support FOR UPDATE; use MySQL/PostgreSQL for true row locking)
    var price float64
    var stock int
    err = tx.QueryRow(
        "SELECT price, stock FROM products WHERE id = ? FOR UPDATE",
        req.ProductID,
    ).Scan(&price, &stock)
    if err == sql.ErrNoRows {
        return 0, fmt.Errorf("product not found")
    }
    if err != nil {
        return 0, err
    }

    // 2. Check stock
    if stock < req.Quantity {
        return 0, fmt.Errorf("insufficient stock: need %d, have %d", req.Quantity, stock)
    }

    // 3. Deduct stock
    result, err := tx.Exec(
        "UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?",
        req.Quantity, req.ProductID, req.Quantity,
    )
    if err != nil {
        return 0, err
    }
    affected, _ := result.RowsAffected()
    if affected == 0 {
        return 0, fmt.Errorf("concurrent stock shortage")
    }

    // 4. Create order
    total := price * float64(req.Quantity)
    result, err = tx.Exec(
        "INSERT INTO orders (product_id, quantity, total) VALUES (?, ?, ?)",
        req.ProductID, req.Quantity, total,
    )
    if err != nil {
        return 0, err
    }

    orderID, _ := result.LastInsertId()

    // 5. Commit
    if err := tx.Commit(); err != nil {
        return 0, fmt.Errorf("failed to commit transaction: %w", err)
    }

    return orderID, nil
}

// ---------- Report ----------

func (s *InventoryStore) LowStockReport(threshold int) ([]product, error) {
    rows, err := s.db.Query(
        "SELECT id, name, price, stock, created_at FROM products WHERE stock < ? ORDER BY stock ASC",
        threshold,
    )
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var products []product
    for rows.Next() {
        var p product
        if err := rows.Scan(&p.ID, &p.Name, &p.Price, &p.Stock, &p.CreatedAt); err != nil {
            return nil, err
        }
        products = append(products, p)
    }
    return products, rows.Err()
}

// ---------- Main ----------

func main() {
    store, err := NewInventoryStore("./inventory.db")
    if err != nil {
        log.Fatal(err)
    }

    // Add products
    laptopID, _ := store.AddProduct("Laptop", 999.99, 10)
    mouseID, _ := store.AddProduct("Mouse", 29.99, 100)
    fmt.Printf("Products added: laptop=%d, mouse=%d\n", laptopID, mouseID)

    // Place order (transaction-safe)
    orderID, err := store.PlaceOrder(OrderRequest{ProductID: int(laptopID), Quantity: 2})
    if err != nil {
        log.Printf("Order failed: %v\n", err)
    } else {
        fmt.Printf("Order placed successfully: order=%d\n", orderID)
    }

    // Low stock report
    lowStock, _ := store.LowStockReport(20)
    fmt.Printf("Low stock products (%d items):\n", len(lowStock))
    for _, p := range lowStock {
        fmt.Printf("  %s: stock=%d\n", p.Name, p.Stock)
    }
}
⚠️ 注意: SQLite は FOR UPDATE 行ロックをサポートしていません。この句は黙って無視されます。上記のコードは MySQL/PostgreSQL でのみ真の行ロックを実現します。SQLite では、mu.Lock() ミューテックスによって安全性が確保されます。データベース間の互換性を確保するには、行ロックの代わりに条件付き更新 (UPDATE ... WHERE stock >= ?) を使用してください。


❓ よくある質問

Q データベース/SQLとORM(GORMなど)のどちらを選べばよいですか?
A 単純なCRUD操作であれば、GORMの方が高速です。SQLをきめ細かく制御する必要がある場合や、高い並行処理能力、あるいは複雑なクエリが必要な場合は、database/sqlを使用してください。推奨:小規模なプロジェクトではGORMを、大規模なプロジェクトではdatabase/sqlとクエリビルダー(squirrelなど)を併用することをお勧めします。
Q なぜプリコンパイルによってSQLインジェクションが防止されるのですか?
A プリコンパイルでは、送信時にSQLの構造とパラメータが分離されます。データベースはまずSQLテンプレートをコンパイルして(構文構造を決定し)、その後、パラメータを生のデータとしてバインドします。パラメータは決してSQL構文として解析されないため、' OR 1=1は単なる文字列であり、WHERE句にはなりません。
Q Goでのトランザクションにおいて、ACID準拠はどのように確保されるのですか?
A エラーが発生した場合、tx.Rollback() を使用してすべての変更をロールバックします。defer tx.Rollback() + tx.Commit()は標準的なパターンです。つまり、Commitが成功した場合、Rollbackはノーオペレーションとなり、Commitが失敗した場合はトランザクションが自動的にロールバックされます。これにより、原子性が確保されます。
Q 接続プールのパラメータはどのように設定すればよいですか?
A 重要なパラメータは、MaxOpenConns(最大同時接続数)とMaxIdleConns(アイドル接続の上限)です。MaxOpenConns は同時接続数の 2~3 倍に、MaxIdleConnsMaxOpenConns/2 に設定することをお勧めします。また、データベースミドルウェアによって接続が切断されないように、SetConnMaxLifetime を 5 分に設定してください。
Q GoではSQLiteとMySQLは同じように使われますか?
A 基本的には同じです。どちらもdatabase/sqlインターフェースを通じて操作されます。違いは以下の通りです:(1) プレースホルダー:SQLiteは?、MySQLは?、PostgreSQLは$1を使用します; (2) SQL構文の違い(自動インクリメントされる主キーの構文など); (3) SQLiteの並行処理性能はMySQLほど良くありません。
Q rows.Next() をすべて処理した後でも、rows.Close() を呼び出す必要がありますか?
A はい。すべての行を処理し終えた後でも、rowsはデータベース接続を保持したままです。rows.Close()は接続をプールに解放します。rowsが作成された直後にdefer rows.Close()を使用してこれを登録することで、どのような状況でもリソースが確実に解放されるようにしてください。
Q 処理が遅いSQLクエリのトラブルシューティングはどのように行えばよいですか?
A (1) データベース/SQL層でdb.SetMaxOpenConnsdb.SetMaxIdleConnsを設定し、接続プールがボトルネックにならないようにします; (2) データベースに組み込まれているスロークエリログを使用する; (3) EXPLAIN ANALYZE を使用してクエリプランを分析する; (4) すべてのクエリの実行時間を記録するために、SQL ロギングミドルウェアの導入を検討する。

📖 まとめ


📝 練習問題

  1. 基本問題(難易度 ⭐):SQLiteデータベースを作成し、tasksというテーブル(id、title、done、created_at)を作成してください。InsertTask、ListTasks(日付順)、MarkDone(idによる更新)の3つの関数を実装してください。

  2. 上級問題(難易度 ⭐⭐)ブログシステムのデータベース層を実装してください。要件:(1) posts テーブル (id, title, content, author_id, created_at); (2) ページネーション付きクエリ (LIMIT/OFFSET) をサポートすること;(3) インジェクション攻撃を防ぐため、挿入時にはプリペアードステートメントを使用すること;(4) トランザクション:投稿が公開された際に、著者の投稿数を更新すること;(5) -race を使用して並行処理の安全性を検証すること。

  3. 課題(難易度:⭐⭐⭐)在庫管理システム向けの並行処理に安全な操作を実装してください。要件:(1) 100個のgoroutineが同時に注文を行う(それぞれが異なる商品の在庫を減少させる); (2) 過剰販売を防ぐために、database/sqlのトランザクションとアプリケーション層のミューテックスを使用すること;(3) すべてのSQL文をプリコンパイルすること;(4) 接続プールを適切に設定すること;(5) 成功した注文と失敗した注文の数をカウントし、最終的な在庫数を算出すること。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%