PHP アドバンス

PHP MySQL プリペアドステートメント

1. PHPにおけるプリペアドステートメントの概要

PHP MySQLのプリペアドステートメント(Prepared Statements)は、主にSQLインジェクション(SQL Injection)攻撃の防止と、クエリ実行のパフォーマンス向上を目的として使用されます。

プリペアドステートメントは、実行したいSQLコマンドから「データ」を分離して扱います。その基本的な仕組みは以下の通りです。

  1. 準備(Prepare): SQLクエリのテンプレートにプレースホルダ(Placeholders)を含めた状態でサーバーに送信します。この時点では実際のデータ値は送信されません。例:INSERT INTO MyGuests VALUES(?, ?, ?)。サーバーはこのテンプレートをパース(解析)、コンパイル、および最適化しますが、まだ実行はしません。
  2. 実行(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内にある疑問符(?)は、firstnamelastnameemail の値を入れるためのプレースホルダです。

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;
?>