タイトル: SQL実行方法
SEOタイトル: PHP で SQL 実行 (PDO/Prepared Statement) 完全ガイド
| この記事の要点 |
|
PHP での SQL 実行: 3 つの方法
| 方式 | 対応 DB | 推奨度 |
|---|---|---|
| PDO | MySQL/PG/SQLite/MSSQL/Oracle 等 | ★★★ 標準 |
| mysqli | MySQL/MariaDB のみ | ★★ 既存システム |
| Laravel Eloquent / Query Builder | PDO 上のラッパー | ★★★ Laravel なら必須 |
| mysql_* (旧) | MySQL | PHP 7 で廃止 - 使用禁止 |
PDO の基本
接続
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_OBJ | stdClass オブジェクト |
PDO::FETCH_CLASS | 指定クラスのインスタンス |
PDO::FETCH_COLUMN | 1 カラムだけ |
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 など) を検討。