23.

PHP で SQL 実行 (PDO/Prepared Statement) 完全ガイド

編集
この記事の要点
  • 現代の PHP では PDO (PHP Data Objects) が標準。MySQL/PostgreSQL/SQLite 等を共通 API で扱える
  • Prepared Statement + バインドパラメータが SQL Injection 対策の基本
  • 取得: $stmt->fetch(PDO::FETCH_ASSOC) / $stmt->fetchAll()
  • トランザクション: beginTransaction() / commit() / rollBack()
  • mysqli は MySQL 専用で残るが新規は PDO 推奨。Laravel なら Eloquent / Query Builder

PHP での SQL 実行: 3 つの方法

方式対応 DB推奨度
PDOMySQL/PG/SQLite/MSSQL/Oracle 等★★★ 標準
mysqliMySQL/MariaDB のみ★★ 既存システム
Laravel Eloquent / Query BuilderPDO 上のラッパー★★★ Laravel なら必須
mysql_* (旧)MySQLPHP 7 で廃止 - 使用禁止

PDO の基本

接続

<?php
$dsn  = 'mysql:host=localhost;port=3306;dbname=mydb;charset=utf8mb4';
$user = 'appuser';
$pass = 'apppass';

$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,  // ★ 例外を投げる
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,        // ★ 連想配列で取得
    PDO::ATTR_EMULATE_PREPARES   => false,                   // ★ 本物の Prepared Statement
    PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8mb4'",
];

try {
    $pdo = new PDO($dsn, $user, $pass, $options);
} catch (PDOException $e) {
    error_log('DB connect failed: ' . $e->getMessage());
    die('Connection failed');
}

// PostgreSQL の場合
$dsn = 'pgsql:host=localhost;port=5432;dbname=mydb';

// SQLite の場合
$dsn = 'sqlite:/var/data/app.db';

SELECT (取得)

// バインドパラメータ (?)
$stmt = $pdo->prepare('SELECT id, name, email FROM users WHERE id = ?');
$stmt->execute([$id]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// → ['id' => 1, 'name' => 'taro', 'email' => 't@b.c']

// 名前付きバインド (:name)
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND status = :status');
$stmt->execute([':email' => $email, ':status' => 'active']);
$user = $stmt->fetch();

// 全件取得
$stmt = $pdo->prepare('SELECT * FROM users WHERE status = ?');
$stmt->execute(['active']);
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);

// 1 行ずつ処理 (大量データ)
$stmt = $pdo->prepare('SELECT * FROM big_table');
$stmt->execute();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    // 1 行ずつ処理 (メモリ効率良い)
}

// 1 カラムだけ取得
$stmt = $pdo->prepare('SELECT COUNT(*) FROM users');
$stmt->execute();
$count = (int) $stmt->fetchColumn();

FETCH_MODE 一覧

モード戻り値
PDO::FETCH_ASSOC連想配列 (推奨)
PDO::FETCH_NUM添字配列 [0=>..., 1=>...]
PDO::FETCH_BOTH両方 (メモリ無駄、非推奨)
PDO::FETCH_OBJstdClass オブジェクト
PDO::FETCH_CLASS指定クラスのインスタンス
PDO::FETCH_COLUMN1 カラムだけ
PDO::FETCH_KEY_PAIR先頭 2 列を key=>value 配列に

INSERT / UPDATE / DELETE

// INSERT
$stmt = $pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
$stmt->execute(['taro', 't@b.c']);
$lastId = (int) $pdo->lastInsertId();

// 名前付き
$stmt = $pdo->prepare('INSERT INTO users (name, email) VALUES (:name, :email)');
$stmt->execute([':name' => 'taro', ':email' => 't@b.c']);

// UPDATE (件数取得)
$stmt = $pdo->prepare('UPDATE users SET name = ? WHERE id = ?');
$stmt->execute(['new_name', $id]);
$updatedRows = $stmt->rowCount();

// DELETE
$stmt = $pdo->prepare('DELETE FROM users WHERE id = ?');
$stmt->execute([$id]);
$deletedRows = $stmt->rowCount();

// 一括 INSERT
$sql = 'INSERT INTO users (name, email) VALUES (?, ?), (?, ?), (?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute(['a', 'a@x', 'b', 'b@x', 'c', 'c@x']);

Prepared Statement と SQL Injection

WHERE 句や VALUES にユーザー入力を直接連結すると SQL Injection 脆弱性になります:

// ❌ 致命的: SQL Injection
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
$pdo->query($sql);
// /?id=1 OR 1=1 → 全件取得
// /?id=1; DROP TABLE users -- → テーブル削除

// ❌ 同じく危険: 文字列連結
$name = $_POST['name'];
$pdo->query("SELECT * FROM users WHERE name = '$name'");
// /?name=' OR '1'='1 → 全件取得

// ✅ 安全: Prepared Statement
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$_GET['id']]);

$stmt = $pdo->prepare('SELECT * FROM users WHERE name = :name');
$stmt->execute([':name' => $_POST['name']]);

// ★ EMULATE_PREPARES = false にすることで本物の Prepared に
// (デフォルトはエミュレートで escape 任せ、本物の方が安全)

bindValue / bindParam で型指定

$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id AND age >= :age');
$stmt->bindValue(':id',  $id,  PDO::PARAM_INT);
$stmt->bindValue(':age', $age, PDO::PARAM_INT);
$stmt->execute();

// 主な型
// PDO::PARAM_INT    : 整数
// PDO::PARAM_STR    : 文字列 (デフォルト)
// PDO::PARAM_BOOL   : 真偽値
// PDO::PARAM_NULL   : NULL
// PDO::PARAM_LOB    : Large Object (BLOB)

// LIMIT に変数を入れるときは bindValue (PARAM_INT) 必須
// (EMULATE_PREPARES=true だと 'LIMIT "10"' になりエラー)
$stmt = $pdo->prepare('SELECT * FROM users LIMIT :limit OFFSET :offset');
$stmt->bindValue(':limit',  $limit,  PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();

トランザクション

try {
    $pdo->beginTransaction();

    $pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')
        ->execute([1000, $from]);

    $pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')
        ->execute([1000, $to]);

    $pdo->prepare('INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)')
        ->execute([$from, $to, 1000]);

    $pdo->commit();
} catch (Throwable $e) {
    if ($pdo->inTransaction()) {
        $pdo->rollBack();
    }
    error_log($e->getMessage());
    throw $e;
}

// 暗黙コミット (DDL) は要注意
// MySQL: BEGIN 中の CREATE TABLE は暗黙 COMMIT する

mysqli (MySQL 専用)

// 接続
$mysqli = new mysqli('localhost', 'user', 'pass', 'mydb');
$mysqli->set_charset('utf8mb4');

// Prepared Statement
$stmt = $mysqli->prepare('SELECT * FROM users WHERE id = ?');
$stmt->bind_param('i', $id);   // 型: i=int, s=string, d=double, b=blob
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
    echo $row['name'];
}

// 多次元 INSERT
$stmt = $mysqli->prepare('INSERT INTO users (name, age) VALUES (?, ?)');
$stmt->bind_param('si', $name, $age);
$stmt->execute();

// 一括処理ループ
foreach ($users as $u) {
    $name = $u['name'];
    $age  = $u['age'];
    $stmt->execute();   // bind_param は参照なので再 bind 不要
}
$stmt->close();
$mysqli->close();

Laravel での SQL 実行

use Illuminate\Support\Facades\DB;
use App\Models\User;

// === Raw SQL ===
$users = DB::select('SELECT * FROM users WHERE status = ?', ['active']);
DB::insert('INSERT INTO logs (msg) VALUES (?)', ['hello']);
$rows = DB::update('UPDATE users SET name = ? WHERE id = ?', ['new', 1]);
$rows = DB::delete('DELETE FROM users WHERE id = ?', [1]);

// === Query Builder ===
$users = DB::table('users')
    ->where('status', 'active')
    ->where('age', '>=', 20)
    ->orderBy('id')
    ->limit(10)
    ->get();

DB::table('users')->insert([
    'name' => 'taro', 'email' => 't@b.c',
]);

DB::table('users')->where('id', 1)->update(['name' => 'new']);
DB::table('users')->where('id', 1)->delete();

// === Eloquent (ORM) ===
$users = User::where('status', 'active')->orderBy('id')->paginate(20);

User::create(['name' => 'taro', 'email' => 't@b.c']);

$user = User::find(1);
$user->name = 'new';
$user->save();

User::destroy(1);

// === Transaction ===
DB::transaction(function () {
    Account::where('id', $from)->decrement('balance', 1000);
    Account::where('id', $to)->increment('balance', 1000);
});

エラーハンドリングのベストプラクティス

// PDO は ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION で例外化
try {
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    return $stmt->fetchAll();
} catch (PDOException $e) {
    // ログには詳細、ユーザーには概要のみ
    error_log(sprintf(
        '[DB] %s SQL=%s PARAMS=%s',
        $e->getMessage(), $sql, json_encode($params)
    ));
    throw new RuntimeException('Database error', 0, $e);
}

// SQLSTATE で原因判別
catch (PDOException $e) {
    if ($e->getCode() === '23000') {        // Integrity constraint violation
        // 重複等
    } elseif ($e->getCode() === '40001') {  // Serialization failure
        // デッドロック → リトライ
    }
    throw $e;
}

FAQ

Q: PDO と mysqli どちらがいい?
A: 新規は PDO。複数 DB を扱う・将来 PostgreSQL 等に切り替える可能性がある場合は特に PDO 一択です。

Q: EMULATE_PREPARES は true / false どちら?
A: false (本物の Prepared Statement) を推奨。エミュレートだと型変換の罠 (LIMIT に文字列が入る等) があり、二重 escape も発生しやすいです。

Q: PDO で接続が切れる
A: MySQL の wait_timeout によるアイドル切断です。PDO::ATTR_PERSISTENT は問題が多く非推奨、必要なら接続プール (PHP-FPM の Persistent Connection など) を検討。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 基本事項
  2. HTMLへの埋め込み
  3. 変数
  4. 可変変数
  5. 定数
  6. データ型
  7. キャスト
  8. エスケープ文字
  9. 配列
  10. 演算子
  11. 代入の際の注意点
  12. 条件分岐
  13. 繰り返し処理
  14. クラスとインスタンス
  15. コンストラクタ
  16. 関数
  17. スーパーグローバル変数
  18. スコープ
  19. staticについて
  20. yieldについて
  21. ファイルのアップロード方法
  22. DB接続方法
  23. SQL実行方法
  24. カプセル化の具体例
  25. 継承の構文
  26. オーバーライド
  27. ポリモーフィズム(多様性)の具体例
  28. 抽象クラス・メソッドの構文と具体例
  29. GET通信
  30. try catchで全てのエラーを拾う方法

最近更新/作成されたページ