PHP MySQL プリペアドステートメント
1. PHPにおけるプリペアドステートメントの概要
PHP MySQLのプリペアドステートメント(Prepared Statements)は、主にSQLインジェクション(SQL Injection)攻撃の防止と、クエリ実行のパフォーマンス向上を目的として使用されます。
プリペアドステートメントは、実行したいSQLコマンドから「データ」を分離して扱います。その基本的な仕組みは以下の通りです。
- 準備(Prepare): SQLクエリのテンプレートにプレースホルダ(Placeholders)を含めた状態でサーバーに送信します。この時点では実際のデータ値は送信されません。例:
INSERT INTO MyGuests VALUES(?, ?, ?)。サーバーはこのテンプレートをパース(解析)、コンパイル、および最適化しますが、まだ実行はしません。 - 実行(Execute): 後からアプリケーションがプレースホルダに値をバインド(紐付け)し、データベースがクエリを実行します。アプリケーションは、異なる値を使用してこのクエリを何度でも繰り返し実行できます。
2. プリペアドステートメントの4つの主要なメリット
プリペアドステートメントを使用することで、以下の利点が得られます。
- パース時間の短縮: クエリの準備(最適化)は一度しか行われないため、同じステートメントを複数回実行する際の処理速度が向上します。
- 帯域幅の最小化: 毎回クエリ全体を送信するのではなく、パラメータのみをサーバーに送信するため、ネットワークの負荷を軽減できます。
- セキュリティの向上: パラメータ値は後から別のプロトコルを使用して転送されるため、正しくエスケープされている必要がありません。元のステートメントテンプレートが外部入力から構築されていない限り、SQLインジェクションは物理的に発生し得ません。
- コードの可読性向上: SQLコマンドとデータが分離されるため、コードがスッキリと整理されます。
3. MySQLiでのプリペアドステートメント実装例
以下の例では、MySQLi拡張機能を使用してプリペアドステートメントを実装しています。
<?php
$servername = "localhost";
$username = "username";
$password = "password";
$dbname = "myDB";
// 接続の作成
$conn = new mysqli($servername, $username, $password, $dbname);
// 接続の確認
if ($conn->connect_error) {
die("接続に失敗しました: " . $conn->connect_error);
}
// SQLクエリのテンプレート
$sql = "INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)";
// SQLクエリのテンプレートを準備(Prepare)
if($stmt = $conn->prepare($sql)) {
// パラメータをバインド(Bind)
$stmt->bind_param("sss", $firstname, $lastname, $email);
// パラメータをセットして実行(1人目)
$firstname = "John";
$lastname = "Doe";
$email = "[email protected]";
$stmt->execute();
// パラメータをセットして実行(2人目)
$firstname = "Mary";
$lastname = "Moe";
$email = "[email protected]";
$stmt->execute();
// パラメータをセットして実行(3人目)
$firstname = "Julie";
$lastname = "Dooley";
$email = "[email protected]";
$stmt->execute();
echo "新しいレコードが正常に作成されました";
} else {
echo "エラー: " . $sql . "<br>" . $conn->error;
}
$stmt->close();
$conn->close();
?>3.1 コードの解説
SQL内にある疑問符(?)は、firstname、lastname、email の値を入れるためのプレースホルダです。
bind_param() 関数に注目してください。この関数は変数をSQLクエリのプレースホルダにバインドします。実行時、プレースホルダは変数に保持されている実際の値に置き換えられます。
引数の "sss" は、各パラメータのデータ型を指定しています。s という文字は、そのパラメータが文字列(String)であることをMySQLに伝えます。全てのパラメータに対して、以下のいずれかの型を定義する必要があります。
i- integer(整数)d- double(浮動小数点数)s- string(文字列)b- binary(画像、PDFなどのバイナリデータ)
MySQLに期待するデータ型を伝えることで、SQLインジェクションのリスクを最小限に抑えることができます。
注: ユーザー入力などの外部ソースからのデータを挿入する場合は、事前にデータのサニタイズ(無害化)とバリデーション(検証)を行うことが非常に重要です。
4. PDOでのプリペアドステートメント実装例
PDOでは、疑問符による指定のほか、名前付き引数による指定も可能です。
4.1 疑問符プレースホルダを使用する場合
<?php
$servername = "localhost";
$username = "username";
$password = "password";
$dbname = "myDB";
try {
$conn = new PDO("mysql:host=$servername;dbname=$dbname", $username, $password);
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $e){
die("接続できませんでした。 " . $e->getMessage());
}
try {
$sql = "INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)";
// SQLテンプレートを準備
$stmt = $conn->prepare($sql);
// 値を指定して実行
$stmt->execute(['John', 'Doe', '[email protected]']);
$stmt->execute(['Mary', 'Moe', '[email protected]']);
$stmt->execute(['Julie', 'Dooley', '[email protected]']);
echo "新しいレコードが正常に作成されました";
} catch(PDOException $e) {
echo "エラー: " . $e->getMessage();
}
$stmt = null;
$conn = null;
?>4.2 名前付き引数(Named arguments)を使用する場合
名前付き引数を使用すると、どの値がどのカラムに対応しているかがより明確になります。
<?php
$servername = "localhost";
$username = "username";
$password = "password";
$dbname = "myDB";
try {
$conn = new PDO("mysql:host=$servername;dbname=$dbname", $username, $password);
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $e){
die("接続できませんでした。 " . $e->getMessage());
}
try {
$sql = "INSERT INTO MyGuests (firstname, lastname, email) VALUES (:firstname, :lastname, :email)";
// SQLテンプレートを準備
$stmt = $conn->prepare($sql);
// パラメータをバインド
$stmt->bindParam(':firstname', $firstname, PDO::PARAM_STR);
$stmt->bindParam(':lastname', $lastname, PDO::PARAM_STR);
$stmt->bindParam(':email', $email, PDO::PARAM_STR);
// 値をセットして実行(1人目)
$firstname = "John";
$lastname = "Doe";
$email = "[email protected]";
$stmt->execute();
// 値をセットして実行(2人目)
$firstname = "Mary";
$lastname = "Moe";
$email = "[email protected]";
$stmt->execute();
// 値をセットして実行(3人目)
$firstname = "Julie";
$lastname = "Dooley";
$email = "[email protected]";
$stmt->execute();
echo "新しいレコードが正常に作成されました";
} catch(PDOException $e) {
echo "エラー: " . $e->getMessage();
}
$stmt = null;
$conn = null;
?>