📢 Webサイト閉鎖と移転のお知らせ
このWebサイトは2026年9月に閉鎖いたします。
新しい記事は移転先で追加しております。(旧サイトでは記事を追加しておりません)

 
(同じ利用者による、間の34版が非表示)
1行目: 1行目:
== 概要 ==
== 概要 ==
PDO(PHP Data Objects)拡張モジュールは、PHPからデータベースにアクセスするための軽量で高性能なインターフェイスである。<br>
データベースの種類により、使用する関数を区別していた。(例. MySQL : mysqli_connect関数、PostgreSQL : pg_connect関数)<br>
もし、異なるデータベースへ変更・移植する場合は、全て書き換えなくてはならない。<br>
<br>
PDO(PHP Data Objects)を使用することで、データベースの種類を問わず同一のクラスやメソッドを使用することができる。<br>
<br>
PDOは、PHPからデータベースにアクセスするための軽量で高性能なインターフェイスである。<br>
PDOインターフェイスを実装する各データベースドライバは、正規表現のようなデータベース固有の機能を提供する。<br>
PDOインターフェイスを実装する各データベースドライバは、正規表現のようなデータベース固有の機能を提供する。<br>
ただし、PDO拡張モジュールは、データベースのあらゆる関数を実行できるわけではない。<br>
<br>
<br>
データベースサーバにアクセスするには、データベース固有のPDOドライバを使用する必要がある。<br>
ただし、PDOは、データベースのあらゆる関数を実行できるわけではない。<br>
例えば、データベースにアクセスする場合は、データベース固有のPDOドライバを使用する必要がある。<br>
<br>
<br>
PDOは、データアクセスの抽象化レイヤを提供する。<br>
PDOは、データアクセスの抽象化レイヤを提供する。<br>
つまり、使用しているデータベースの種類に関わらず、同じ関数を使用してクエリの発行やデータの取得が行うことができる。<br>
つまり、使用しているデータベースの種類に関わらず、同じ関数を使用してクエリの発行やデータの取得が行うことができる。<br>
<br>
<br>
PDOは、PHP 5.1以降にバンドルされており、PHP 5.0ではPECL拡張モジュールとして使用可能である。<br>
<u>PDOは、PHP 5.1以降にバンドルされており、PHP 5.0ではPECL拡張モジュールとして使用可能である。</u><br>
PHP 5の新機能であるオブジェクト指向で設計されているため、それより前のPHPでは動作しない。<br>
<u>PHP 5の新機能であるオブジェクト指向で設計されているため、それより前のPHPでは動作しない。</u><br>
<br>
ここでは、PDOを使用して、MySQLへ接続して操作する手順を記載する。<br>
<br><br>
<br><br>


54行目: 61行目:
<br><br>
<br><br>


== データベースの接続と管理 ==
== データベース処理のアンチパターン ==
以下にどれか1つでも当てはまるソースコードは見直す必要がある。<br>
太字にしてあるものは、脆弱性に直結する危険度の高いものである。<br>
* <code>mysql_query</code>等の非推奨関数を使用している。
*: <code>mysql_</code>で始まる関数は、PHP5.5で非推奨となっている。
*: <br>
* MySQLを使用している場合、<code>SET NAMES</code>あるいは<code>SET CHARACTER SET</code>等で文字コードを指定している。<br>または、データベースで使用する文字コードが未指定である。
*: <code>SET NAMES</code>等の使用は避けるべきである。
*: これは、データベース側の文字セットを変更するだけであり、PDOドライバの文字セットは無関係なため、
*: 入力側と出力側で文字セットが異なる場合は脆弱性が発生する原因になる。
** 文字コードの性質上、<code>SET NAMES sjis</code>や<code>SET CHARACTER SET sjis</code>と設定する場合、顕著に脆弱性が現れる。
** <code>SET NAMES utf8</code>や<code>SET CHARACTER SET utf8</code>は基本的には安全だが、例外もあるため注意すること。<br>MySQLにおいては、libmysqlclientのコンパイルオプションに<code>--with-charset=cp932</code>や<code>--with-charset=sjis</code>を指定している場合が該当する。<br>mysqlndを使用している場合は問題無い。
*: <br>
* '''<code>SELECT * FROM users WHERE id = '$id'</code>のように変数展開を使用してSQL文を組み立てている。'''
*: <br>
* <code>$_POST['id']</code>等の外部入力の変数が定義されているか確認していない。<br>外部入力の変数が文字列であるか確認していない。<br>'''また、外部入力の変数を出力する時、<code>htmlspecialchars</code>関数を使用していない。
*: 以下のようなPHPスクリプトを記述してはならない。
<syntaxhighlight lang="php">
// レコードを挿入する前にhtmlspecialchars関数を使用している間違った例
$stmt = $pdo->prepare('INSERT INTO users(name) VALUES(?)');
$stmt->bindValue(1, htmlspecialchars($name), PDO::PARAM_STR);
</syntaxhighlight>
<br>
* SQLの<code>LIKE</code>演算子を使用しているにも関わらず、<code>%</code>、<code>_</code>、<code>\</code>にエスケープ文字を付加していない。
*: <br>
* HTMLの<code>body</code>タグの中に、データベースの接続処理を記述している。<br><code>echo</code>関数や<code>print</code>関数を、そのまま記述している。<br>Content-Typeが<code>text/plain</code>ではなく、<code>exit</code>関数や<code>die</code>関数を使用して強制終了の処理を記述している。
*: ロジックとテンプレートの分離を実施する。
*: これは、ファイルの先頭で全てのロジックを記述して、その後の処理でHTMLタグを出力する。(ファイルも分割すべきである)
*: ロジックとテンプレートの分離を実施した後、<u>HTMLタグの中にPHPスクリプトを記述する手法</u>を用いる。
*: 以下の例では、ロジックとテンプレートを分離して、データベースにアクセスしてレコードを取得している。
<syntaxhighlight lang="php">
<?php
    try
    {
      $pdo = new PDO('mysql: ... ', 'root', 'password', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,]);
      $rows = $pdo->query('SELECT * FROM users')->fetchAll(PDO::FETCH_ASSOC);
    }
    catch (PDOException $e)
    {
      exit($e->getMessage());   
    }
    function h($str)
    {
      return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
    }
?>
<!DOCTYPE html>
<html>
    <head>
      <meta charset="utf-8">
      <title>Example</title>
    </head>
    <body>
      <ul>
      <?php foreach ($rows as $row): ?>
          <li><?=h($row)?></li>
      <?php endforeach; ?>
          </ul>
    </body>
</html>
</syntaxhighlight>
<br>
: もし、exit関数やdie関数を使用する場合は、その直前において、以下のように記述する。
: 500は<code>500 Internal Server Error</code>の意味で、サーバ側が原因でエラーになったことを表す。
<syntaxhighlight lang="php">
header('Content-Type: text/plain; charset=UTF-8', true, 500);
</syntaxhighlight>
<br><br>
 
== データベースの接続 ==
==== 接続文字列 ====
PDOクラスのインスタンスを作成することにより、接続が確立される。<br>
PDOクラスのインスタンスを作成することにより、接続が確立される。<br>
コンストラクタには、データソース(DSN)、ユーザ名、パスワードを指定する。<br>
コンストラクタには、データソース(DSN)、ユーザ名、パスワードを指定する。<br>
<br>
<br>
==== MySQLへの接続 ====
データベース接続時になんらかのエラーが発生した場合、<code>PDOException</code>クラスがスローされる。<br>
データベース接続時になんらかのエラーが発生した場合、<code>PDOException</code>クラスがスローされる。<br>
エラー処理を行う場合はこの例外をキャッチする。<br>
エラー処理を行う場合はこの例外をキャッチする。<br>
75行目: 153行目:
<u>PDOのインスタンスへの参照(<code>PDOStatement</code>クラスのインスタンスからの参照や別のPDOのインスタンスからの参照等)が残っている場合、</u><br>
<u>PDOのインスタンスへの参照(<code>PDOStatement</code>クラスのインスタンスからの参照や別のPDOのインスタンスからの参照等)が残っている場合、</u><br>
<u>それらも併せて削除する必要がある。(<code>PDOStatement</code>クラスのインスタンスに<code>null</code>を代入する等)</u><br>
<u>それらも併せて削除する必要がある。(<code>PDOStatement</code>クラスのインスタンスに<code>null</code>を代入する等)</u><br>
<br>
以下に、データベースへの接続を行う時のPDOのインスタンスの生成手順を記載する。<br>
<syntaxhighlight lang="php">
$<PDOクラスのインスタンス名> = new PDO(<データソース名>, <データベースユーザ名>, <パスワード>, <ドライバオプション>);
PDO PDO::__construct ( string dsn
                        [, string username
                        [, string password
                        [, array driver_options]]] )
指定されたデータベースへの接続を表すPDOインスタンスを生成します。
引数:
  dsn
    データソース名(Data Source Name)またはDSN。データベースに接続するために必要な情報が含まれる。
  username
    DSN文字列のユーザ名。このパラメータは、いくつかのPDOドライバではオプションである。
  password
    パスワード。
    DSN文字列で必要とされる場合に指定。
  driver_options
    ドライバ固有の接続オプションを指定するキー=> 値の配列
戻り値:
  成功時にPDOオブジェクトを返す。
</syntaxhighlight>
<br>
* データソース名(Data Source Name)
*: データベースに接続するために必要な情報。
*: PHP Manualに各データベースに応じたDSNの記述方法が掲載されている。
*: 先頭にデータベースの種類を指定して、<code>:</code>(コロン)で区切る。
*: 各項目は、<code>項目名=値</code>として、<code>;</code>(セミコロン)で区切る。
*: <br>
*: 以下の例は、PHP 5.3.6以降のMySQLにおけるデータソース名の記述方法である。(ポート番号は省略可能)
*: PHP 5.3.5以前では、charsetは使用できない。
*: <code>mysql:dbname=<データベース名>;host=<ホスト名またはIPアドレス>:port=<ポート番号>;charset=<文字コード></code>
*: 例. <code>mysql:dbname=SampleDB;host=localhost:port=3306;charset=utf8mb4</code>
*: <br>
** dbname
**: データベース名を指定する。(基本的には必須であるが、後で<code>USE <データベース名>;</code>とクエリを実行する場合は省略できる)
**: <br>
** host
**: ホスト名またはIPアドレスを指定する。
**: (ローカル環境のみで実行する場合、省略しても問題ない場合がある)
**: Linuxでは、ホスト名を指定することが推奨される。
**: http://qiita.com/mpyw/items/b00b72c5c95aac573b71#comment-e9db50fff9bffa1dd6f8
**: Windowsでは、IPアドレスを指定することが推奨される。
**: http://www.ah-2.com/2012/01/28/win_localhost_slow.html
**: <br>
** charset
**: 文字コードを指定する。(<code>SET NAMES</code>は使用しないこと)
**: <u>UTF-8を指定する場合、<code>utf8</code>であることに注意すること。</u>
**: <u>MySQL5.5.3以降を使用する場合、4バイトからなる絵文字等も取り扱える<code>utf8mb4</code>を使用することを強く推奨する。</u>
**: <br>
* データベースユーザ名
*: データベースのユーザ名を指定する。
*: <br>
* パスワード
*: データベースユーザのパスワードを指定する。
*: <br>
* ドライバオプション
*: 接続時のオプションを連想配列で渡す。
*: キーは予め用意されている定数を使用する。
*: 値は予め用意されている定数以外に、論理値や文字列等の一般的な値でもよい。
<br>
==== ドライバオプション ====
ドライバオプションとその値を以下に示す。<br>
<br>
<syntaxhighlight lang="php">
// ドライバオプションの使用例
$options = [
    // エラー発生時に例外をスロー
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    // フェッチモードをデフォルトで連想配列に設定
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    // プリペアドステートメントを有効化
    PDO::ATTR_EMULATE_PREPARES => false,
    // 持続的な接続を無効化
    PDO::ATTR_PERSISTENT => false,
    // カラム名を小文字に変換
    PDO::ATTR_CASE => PDO::CASE_LOWER
];
$db = new PDO(
    'mysql:host=localhost;dbname=sampledb;charset=utf8mb4',
    'root',
    'root',
    $options
);
</syntaxhighlight>
<br>
* PDO::ATTR_ERRMODE
*: エラーレポートの方法を設定するための属性である。
*: クエリの実行でエラーが起きた場合、どのように処理をするかを指定する。
*: 初期値は、<code>PDO::ERRMODE_SILENT</code>である。
** PDO::ERRMODE_EXCEPTION
**: エラーが発生した時に例外 (PDOException例外クラス) をスローする。
**: エラーを例外としてキャッチできるようになり、デバッグが容易になる。
** PDO::ERRMODE_WARNING
**: クエリの実行Lで発生したエラーをPHPのWarningとして報告する。
**: <code>PDOStatement::execute</code>メソッドの戻り値が<code>false</code>かどうかを毎回確認する必要がある。
** PDO::ERRMODE_SILENT
**: 何も報告しない。
**: <code>PDOStatement::execute</code>メソッドの戻り値が<code>false</code>かどうかを毎回確認する必要がある。
*: <br>
* PDO::ATTR_DEFAULT_FETCH_MODE
*: SELECT結果を連想配列で取得できる。
*: キーがカラム名になるため、データアクセスが直観的になる。
*: <br>
*: <code>PDOStatement::fetch</code>メソッドや<code>PDOStatement::fetchAll</code>メソッドで引数が省略された場合、
*: または、ステートメントが<code>foreach</code>文に直接かけられた場合のフェッチスタイルを設定する。
*: 初期値は、<code>PDO::FETCH_BOTH</code>である。
** PDO::FETCH_BOTH
**: カラム番号とカラム名の両方をキーとする連想配列で取得する。
** PDO::FETCH_NUM
**: カラム番号をキーとする配列で取得する。
** PDO::FETCH_ASSOC
**: カラム名をキーとする連想配列で取得する。この設定が最も使用される。
** PDO::FETCH_OBJ
**: カラム名をプロパティとする基本オブジェクトで取得する。
*: <br>
* PDO::ATTR_EMULATE_PREPARES
*: データベース側が持つプリペアドステートメント機能のエミュレーションをPDO側で行うかどうかを設定する。
*: PHP 5.2以降の初期値は<code>true</code>である。
*: <br>
*: <code>false</code>の場合、ネイティブのプリペアドステートメントを使用する。
*: これは、SQLインジェクション対策として有効であり、また、クエリのパフォーマンスが向上する。
*: <br>
*: この設定は、いくつかPDOの挙動に違いが現れる。
*: エミュレーションを行う場合、プリペアドステートメントのためにデータベースと通信する必要が無くなるため、パフォーマンスは向上する。
*: 存在しないテーブル名 / カラム名をクエリに持つプリペアドステートメントを発行する場合、
*: エミュレーションを行わない場合は即エラーが発生するが、エミュレーションを行う場合はクエリを実行するまでエラーが発生するかどうかわからない。
** エミュレーションを行う場合のみ、<code>;</code>(セミコロン)区切りで複数のクエリを1つのクエリで実行することができる。
*: <br>
* PDO::ATTR_PERSISTENT (コンストラクタでの指定のみ)
*: <code>true</code>の場合、PHPスクリプトが終了してもデータベースへの接続を維持して、次回に再利用する。
*: 特に、大規模システムでは恩恵が大きい。
*: <br>
*: <code>false</code>の場合は、持続的な接続を無効化する。
*: メモリ管理の観点から、通常は無効にすることを推奨する。
*: <br>
* PDO::MYSQL_ATTR_USE_BUFFERED_QUERY(MySQL専用)
*: <code>true</code>の場合、バッファクエリを使用する。初期値はMySQLの各バージョンによって異なる。
** バッファクエリ
**: 全ての情報をデータベースから取得して、PHPスクリプトにより1件ずつ取得する。
** 非バッファクエリ:
**: 1件ごとにデータベースサーバと通信を行って,PHPに取り出させる
*: 取得してくる情報がメモリに収まりきらない莫大なデータ量といった特殊なケースを除けば、バッファクエリを選択した方がよい。
*: バッファクエリを設定すると、サーバ負荷も軽減されて、途中までフェッチしたところで突然例外が発生するような事態も避けられる。
*: 非バッファクエリは、データベースから取得したデータを、コマンドラインからバッチ処理を実行する用途で使用されるが、
*: 複数同時にクエリを実行できない等の大きな欠点もある。
*: <br>
* PDO::MYSQL_ATTR_INIT_COMMAND(MySQL専用、コンストラクタでの指定のみ)
*: データベースに接続した直後に実行されるクエリをここに記述する。
*: <br>
* PDO::ATTR_CASE
*: <code>PDO::CASE_LOWER</code>を指定する場合、カラム名を小文字に統一する。
*: そのため、一貫性のあるコーディングが可能になる。
<br>
  <syntaxhighlight lang="php">
  <syntaxhighlight lang="php">
  <?php
  <?php
     try
     try
     {
     {
       $dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
      // PHP 5.3.6以降
       $dbh = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', $user, $pass,
                      [
                        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                        PDO::ATTR_EMULATE_PREPARES => false,
                      ]);
      // PHP 5.3.5以前
      //$dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass,
      //              [
      //                  PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
      //                  PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
      //                  PDO::ATTR_EMULATE_PREPARES => false,
      //              ]);
      //$dbh->exec("SET NAMES utf8mb4");
       foreach($dbh->query('SELECT * from FOO') as $row)
       foreach($dbh->query('SELECT * from FOO') as $row)
       {
       {
88行目: 349行目:
     catch(PDOException $e)
     catch(PDOException $e)
     {
     {
      // エラーが発生した場合、"500 Internal Server Error"を表示して終了する
      // もし、任意のエラー画面を表示する場合、HTMLの表示を継続する
      // ここではエラー内容を表示しているが、実際の商用環境ではログファイルに記録してWebブラウザには表示しない方が望ましい
      header('Content-Type: text/plain; charset=UTF-8', true, 500);
       print "エラー!: " . $e->getMessage() . "<br/>";
       print "エラー!: " . $e->getMessage() . "<br/>";
       die();
       die();
     }
     }
  ?>
  ?>
// Webブラウザにheader関数以降に表示するHTMLがUTF-8で記述されていることを認識させる
// または、header関数の代わりに<meta charset="utf-8">を記述する
// 両方記述してもよい
header('Content-Type: text/html; charset=utf-8');
<!DOCTYPE html>
<html>
    <head>
      <meta charset="utf-8">
      <title>Example</title>
    </head>
    <body>
      <!-- ここではHTMLタグ以外を記述しない -->
    </body>
</html>
  </syntaxhighlight>
  </syntaxhighlight>
<br>
<br>
==== データベースの接続を閉じる ====
<u>PDOクラスのコンストラクタは、<code>PDO::ERRMODE_EXCEPTION</code>の有無に関わらず<code>PDOException</code>をスローするが、</u><br>
データベースの接続は、PHPスクリプトが終了しても閉じられずにキャッシュされ、他のPHPスクリプトが同じ内容の接続を要求する時に再利用される。<br>
<u>データベースに接続できない場合等は、Warningが発生する。</u><br>
このキャッシュにより、新しい接続を確立するオーバーヘッドを避けることができるため、Webアプリケーションを高速化できるようになる。<br>
<u>もし、データベースを動的に指定する場合、<code>set_error_handler</code>関数を使用して<code>ErrorException</code>に変換した後、例外処理を行う。</u><br>
<br>
 
==== 文字コードの設定 ====
<u>PDOでMySQLに接続する場合は、文字コードの設定 (例: charset=utf8mb4) を指定することを強く推奨する。</u><br>
<br>
もし、明示的に指定しない場合、MySQLのデフォルト設定に依存することになり、環境によって動作が異なる可能性がある。<br>
そのため、確実な文字エンコーディングの制御のために、charsetを明示的に指定することを推奨する。<br>
<br>
* 絵文字や特殊文字のサポート
*: utf8mb4は絵文字やその他の4バイト文字を完全にサポートする。
*: 通常のutf8では絵文字などが正しく保存できない可能性がある。
*: <br>
* 文字化け防止
*: 日本語を含む多言語対応において、文字化けを防ぐことができる。
*: データベースとアプリケーション間で一貫した文字エンコーディングを保証する。
*: <br>
* 将来の互換性
*: 現代のWebアプリケーションでは、utf8mb4が事実上の標準となっている。
*: 後々データベースに絵文字等を格納する可能性を考慮すると、最初から設定しておくのが無難である。
<br>
  <syntaxhighlight lang="php">
  <syntaxhighlight lang="php">
  <?php
  // 使用例
    $dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
   
    $sth = $dbh->query('SELECT * FROM foo');
   
   
    // 使用を終了したので、閉じます
$db = new PDO('mysql:host=<IPアドレスまたはホスト名>;dbname=<データベース名>;charset=utf8mb4', '<ユーザ名>', '<パスワード>');
    $sth = null;
    $dbh = null;
?>
  </syntaxhighlight>
  </syntaxhighlight>
<br>
<br><br>
==== 持続的な接続 ====
 
== データベースの持続的な接続 ==
<code>PDO::ATTR_PERSISTENT</code>オプションの値は、string型の値が設定されない限り、bool型(持続的な接続の有無を示す)に変換される。<br>
<code>PDO::ATTR_PERSISTENT</code>オプションの値は、string型の値が設定されない限り、bool型(持続的な接続の有無を示す)に変換される。<br>
string型の値を設定する場合、複数の接続プールを使用することができる。<br>
string型の値を設定する場合、複数の接続プールを使用することができる。<br>
130行目: 426行目:
     $dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass, array(PDO::ATTR_PERSISTENT => true));
     $dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass, array(PDO::ATTR_PERSISTENT => true));
  ?>
  ?>
</syntaxhighlight>
<br><br>
== データベースの切断 ==
データベースの接続は、PHPスクリプトが終了しても閉じられずにキャッシュされ、他のPHPスクリプトが同じ内容の接続を要求する時に再利用される。<br>
このキャッシュにより、新しい接続を確立するオーバーヘッドを避けることができるため、Webアプリケーションを高速化できるようになる。<br>
<br>
もし、明示的に閉じる場合、作成したPDOクラスのオブジェクトにNULLを代入する。<br>
<syntaxhighlight lang="php">
<?php
    try
    {
      $dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass,
                      [
                        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
                      ]);
      foreach($dbh->query('SELECT * from FOO') as $row)
      {
          print_r($row);
      }   
    }
    catch (PDOException $e)
    {
      print('Connection failed:'.$e->getMessage());
      die();
    }
    // データベースの切断
    $dbh = null;
?>
</syntaxhighlight>
<br><br>
== クエリの実行 ==
==== PDO::queryメソッド ====
ユーザ入力を伴わないクエリは、PDO::queryメソッドを実行する。<br>
戻り値は、PDOStatementクラスである。<br>
<syntaxhighlight lang="php">
$stmt = $pdo->query('SELECT * FROM users');
</syntaxhighlight>
<br>
==== PDO::execメソッド ====
ユーザ入力を伴わないクエリにおいて、INSERTやUPDATE等を使用してレコードを直接取得する場合、PDO::execメソッドを使用する。<br>
また、結果を必要としない場合においても、PDO::execメソッドを使用すべきである。<br>
<br>
以下の例では、テーブルに存在する全てのレコードを更新して、更新した全てのレコード数を取得している。<br>
<syntaxhighlight lang="php">
$count = $pdo->exec('UPDATE users SET age = age + 1');
</syntaxhighlight>
<br>
==== PDO::executeメソッド(3ステップ) ====
<code>PDO::prepare</code>メソッド → <code>PDOStatement::bindValue</code>メソッド → <code>PDOStatement::execute</code>メソッドの3ステップでクエリを実行する。<br>
ユーザ入力からクエリを動的に作成する場合、プリペアドステートメントとプレースホルダを使用する。<br>
* プレースホルダ
*: ユーザ入力を代入する場所として、予め確保するためのもの。
*: プレースホルダには2種類あり、<u>疑問符プレースホルダを使用する方法</u>と<u>名前付きプレースホルダ使用する方法</u>がある。
*: これらを混ぜて使用する場合はエラーとなる。
* プリペアドステートメント
*: プレースホルダを使用するために作成する。
<br>
* 疑問符プレースホルダ<br>
*: ?の要素は、1から始まる。<br>
*: <code>PDO::PARAM_STR</code>は省略することができる。<br>
*: エミュレーションが有効の場合、正しくキャストできない場合があるため、文字列以外を扱う時は明示的なキャストが必要である。<br>
*: NULL値に関しては、<code>PDO::PARAM_NULL</code>が暗黙的に使用される。<br>
<syntaxhighlight lang="php">
// エミュレーションが有効の場合
$stmt = $pdo->prepare('SELECT * FROM users WHERE gender = ? AND age = ?');
$stmt->bindValue(1, $gender);
$stmt->bindValue(2, (int)$age, PDO::PARAM_INT);
$stmt->execute();
// エミュレーションが無効の場合、上記または下記のいずれの記述も可能
<syntaxhighlight lang="php">
$stmt = $pdo->prepare('SELECT * FROM users WHERE gender = ? AND age = ?');
$stmt->bindValue(1, $gender);
$stmt->bindValue(2, $age, PDO::PARAM_INT);
$stmt->execute();
</syntaxhighlight>
<br>
* 名前付きプレースホルダ
*: <code>:</code>(コロン)を先頭に付加して、半角英数字とアンダースコアにて構成する。
*: バインド時の先頭は、<code>:</code>(コロン)は省略できる。
*: エミュレーションが有効の場合、明示的なキャストが必要である。
<syntaxhighlight lang="php">
// エミュレーションが有効の場合
$stmt = $pdo->prepare('SELECT * FROM users WHERE age = :age AND gender = :gender');
$stmt->bindValue(':age', (int)$age, PDO::PARAM_INT);
$stmt->bindValue(':gender', $gender);
</syntaxhighlight>
<br>
<syntaxhighlight lang="php">
// バインド時において、先頭の:(コロン)を省略する場合
$stmt->bindValue('gender', $gender);
</syntaxhighlight>
<br>
<syntaxhighlight lang="php">
// IDが20で年齢も20歳の人を取得
$n = 20;
$stmt = $pdo->prepare('SELECT * FROM users WHERE age = :n AND id = :n');  // エミュレーションが有効の場合のみ、同名のプレースホルダを複数使用できる
$stmt->bindValue(':n', (int)$age, PDO::PARAM_INT);
$stmt->execute();
</syntaxhighlight>
<br>
なお、 値を即時にバインドせずに、変数を参照的にバインドしておき、実行時に値をバインドする<code>PDOStatement::bindParam</code>メソッドも存在するが、<br>
<code>PDOStatement::bindParam</code>メソッドを使用する必要はない。<br>
エミュレーションが有効の場合、実行後にバインドした変数が文字列型に変換する仕様もあるので、注意すること。<br>
<br>
==== PDO::executeメソッド(2ステップ) ====
<code>PDO::prepare</code>メソッド → <code>PDOStatement::execute</code>メソッドの2ステップでクエリを実行する。<br>
<code>PDOStatement::execute</code>メソッドの引数に配列を渡す場合、それらを全てバインドした後そのままクエリが実行される。<br>
<br>
ただし、以下の条件に注意すること。<br>
<u>NULL値以外は全て<code>PDO::PARAM_STR</code>扱いになる。</u><br>
もし、間違った型でバインドする場合はMySQL / SQLiteはデータベース側で自動的にキャストし直すが、<br>
パフォーマンスの低下やバグの原因になるため、可能な限り避けること。(PostgreSQLの場合はエラーになる)<br>
<br>
また、既に<code>PDOStatement::bindValue</code>メソッドで値がバインドされている場合でも、それらは全て無視される。<br>
これを用いる場合、全てのバインドをこの引数で行わなければならない。<br>
<br>
* 疑問符プレースホルダ
*: <code>PDOStatement::bindValue</code>メソッドとは異なり、<code>?</code>のインデックスは0から始まる。
<syntaxhighlight lang="php">
$stmt = $pdo->prepare('SELECT * FROM users WHERE city = ? AND gender = ?');
$stmt->execute([$city, $gender]);
// キーを設定して順番を変えて指定することもできる
$stmt->execute([1 => $gender, 0 => $city]);
</syntaxhighlight>
<br>
* 名前付きプレースホルダ
<syntaxhighlight lang="php">
$stmt = $pdo->prepare('SELECT * FROM users WHERE city = :city AND gender = :gender');
$stmt->execute([':city' => $city, ':gender' => $gender]);
// 先頭のコロンは省略できる
$stmt->execute(['city' => $city, 'gender' => $gender]);
// compact関数を使用する場合
$stmt->execute(compact('city', 'gender'));
  </syntaxhighlight>
  </syntaxhighlight>
<br><br>
<br><br>
166行目: 606行目:
       $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
       $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
   
   
      // トランザクション開始
       $dbh->beginTransaction();
       $dbh->beginTransaction();
      // SQLクエリの実行
       $dbh->exec("insert into staff (id, first, last) values (23, 'Joe', 'Bloggs')");
       $dbh->exec("insert into staff (id, first, last) values (23, 'Joe', 'Bloggs')");
       $dbh->exec("insert into salarychange (id, amount, changedate) values (23, 50000, NOW())");
       $dbh->exec("insert into salarychange (id, amount, changedate) values (23, 50000, NOW())");
      // 正常に実行できた場合はコミット
       $dbh->commit();
       $dbh->commit();
     }
     }
     catch(Exception $e)
     catch(Exception $e)
     {
     {
       $dbh->rollBack();
       // エラーが発生した場合はロールバック
      if ($dbh->inTransaction()) {
          $dbh->rollBack();
      }
       echo "失敗しました。" . $e->getMessage();
       echo "失敗しました。" . $e->getMessage();
     }
     }
187行目: 635行目:
<u>トランザクションを開始するクエリを手動で発行した場合、PDOはそれを知ることができないため、問題が発生してもロールバックできない。</u><br>
<u>トランザクションを開始するクエリを手動で発行した場合、PDOはそれを知ることができないため、問題が発生してもロールバックできない。</u><br>
<br><br>
<br><br>
== プリペアドステートメント ==
プリペアドステートメントとは、実行するSQLをコンパイルしたテンプレートのようなものである。<br>
パラメータ変数を使用することで、SQLをカスタマイズすることができる。<br>
<br>
プリペアドステートメントには、以下に示す2つの大きな利点がある。<br>
* 一般的に、クエリを実行する時、クエリの解析、コンパイル、実行プランの最適化が行われる。<br>プリペアドステートメントを使用する場合、これらの解析、コンパイル、最適化の繰り返しを避けることができる。<br>端的に言うと、プリペアドステートメントは使用するリソースが少ないため、高速に動作するということである。<br><br>
* プリペアドステートメントに渡すパラメータは、引用符で括る必要は無く、ドライバが自動的に行う。<br>プリペアドステートメントでは、SQLインジェクションは発生しない。
<br>
プリペアドステートメントは、データベースの種類や機能に関わらず同じ仕組みで データベースへのアクセスができる。<br>
<br><br>
== プリペアドステートメントの種類 ==
==== bindValueメソッド (値渡し) ====
* メリット
*: バインドした後に元の変数が変更されても影響を受けない。
*: 直接リテラル値をバインドできる。
*: メモリ効率が良い。 (参照を保持しない)
* デメリット
*: ループ内で変数を変更して再実行する場合は、その都度、bindValueメソッドが必要となる。
<br>
<syntaxhighlight lang="php">
$value = 100;
$stmt->bindValue(1, $value, PDO::PARAM_INT);
$value = 200;      // この変更はSQLに影響しない
$stmt->execute();  // 100 が使用される
</syntaxhighlight>
<br>
==== bindParamメソッド (参照渡し) ====
* メリット
*: 変数の参照を保持するので、値を変更して再実行が容易
*: ストアドプロシージャのOUTパラメータで使用可能
* デメリット
*: リテラル値を直接バインドできない。
*: メモリを若干多く使用する。 (参照を保持するため)
<br>
<syntaxhighlight lang="php">
$value = 100;
$stmt->bindParam(1, $value, PDO::PARAM_INT);
$value = 200;      // この変更がSQLに反映される
$stmt->execute();  // 200 が使用される
</syntaxhighlight>
<br>
==== executeメソッドで直接記述 ====
* メリット
*: コードが簡潔
*: 1回のみ使用する場合は効率的
*: <br>
* デメリット
*: 変数の値を後から変更して再実行する場合は、その都度配列を作る必要がある。
*: 参照渡しができない。
<br>
<syntaxhighlight lang="php">
$stmt = $pdo->prepare("UPDATE student SET id = ?, name = ?, grade = ? WHERE id = ?");
$stmt->execute([$id, $name, $grade, $old_id]);  // シンプル!
## 1回限りのクエリの場合、executeメソッド
$stmt->execute([$id, $name, $grade]);
</syntaxhighlight>
<br>
==== 使い分け ====
<syntaxhighlight lang="php">
# 使い分け
## 一般的なケースでは、bindValueメソッドを使用
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bindValue(1, $id, PDO::PARAM_INT);
$stmt->execute();
## 以下に示す場合は、bindParamメソッドを使用
## ループ内で値を変更しながら実行する場合
## ループ内で変数を変更しながら再実行する場合
## ストアドプロシージャのOUTパラメータを使用する場合
## 特別な理由で参照渡しが必要な場合
$stmt = $pdo->prepare("INSERT INTO logs (user_id, action) VALUES (?, ?)");
$stmt->bindParam(1, $user_id, PDO::PARAM_INT);
$stmt->bindParam(2, $action, PDO::PARAM_STR);
foreach($actions as $action) {
    $stmt->execute();  // $actionの値が自動的に使用される
}
</syntaxhighlight>
<br>
<u>セキュリティが重要な場合は、型を明示的に指定できるbindValueメソッド、または、bindParamメソッドを使用する。</u><br>
<br>
<u>※注意</u><br>
<u>ただし、実務では一貫性を保つため、特別な理由がない限りどちらか一方に統一することが推奨される。</u><br>
<br><br>
== レコードの取得(プリペアドステートメント) ==
<center>
{| class="wikitable" style="background-color:#fefefe;"
|-
! style="background-color:#00ffff;" | MySQL
! style="background-color:#00ffff;" | PHPのデータ型<br>(エミュレーション無しmysqlnd)
! style="background-color:#00ffff;" | PHPのデータ型(libmysqlclient)<br>(エミュレーション有りmysqlnd)
|-
| NULL || NULL || NULL
|-
| 文字列 || String || String
|-
| 日付 || String || String
|-
| タイムスタンプ || Integer || String
|-
| 論理値 || Integer || String
|-
| PHPで扱える値の整数 || Integer || String
|-
| PHPで扱えない値の整数 || String || String
|-
| 浮動小数点 || String || String
|}
</center>
<u>※注意</u><br>
<u>エミュレーションに関するオプションは、PDO::ATTR_EMULATE_PREPARESという命名ではあるが、</u><br>
<u>プリペアドステートメントを使用しない場合にも影響が及ぶことに注意すること。</u><br>
<br>
下表に、<code>PDO::setAttribute</code>で取得するデータ型を変更できるものを示す。<br>
<code>PDO::ATTR_ORACLE_NULLS</code>オプションは、Oracle以外のデータベースでも使用できる。<br>
<center>
{| class="wikitable" style="background-color:#fefefe;"
|-
! style="background-color:#00ffff;" | データベース
! style="background-color:#00ffff;" | PDO::NULL_EMPTY_STRING
! style="background-color:#00ffff;" | PDO::NULL_TO_STRING
|-
| NULL || NULL || ""
|-
| "" || NULL || ""
|}
</center>
<br>
また、PDO::ATTR_STRINGIFY_FETCHESオプションをtrueに指定する時、エミュレーションが無効の場合は数値が文字列に変換される。<br>
エミュレーションが有効の場合、設定に関わらず常に数値が文字列に変換される。<br>
<br>
==== PDOStatement::fetchメソッド ====
カーソルを移動して、指定したフェッチモードで1行ずつ取得する。<br>
* 引数を省略する場合、デフォルトフェッチモードが使用される。
* 全てのレコードを取得した場合、falseを返す。
<br>
以下の例では、フェッチモードを<code>PDO::FETCH_ASSOC</code>に設定している。<br>
<syntaxhighlight lang="php">
// 基本的な構文
while($row = $stmt->fetch())
{
    printf("%s lives in %s<br />\n", $row['name'], $row['city']);
}
// vprintf関数を使用する場合
while($row = $stmt->fetch())
{
    vprintf("%s lives in %s<br />\n", $row);
}
</syntaxhighlight>
<br>
デフォルトフェッチモードの場合、<code>PDOStatement</code>クラスは<code>Traversable</code>インターフェースを実装しているため、foreach文で記述することができる。<br>
ただし、HTMLのための変数を用意する場合、配列として持つ方が都合が良いため、<code>PDOStatement::fetchAll</code>メソッドの使用することを推奨する。<br>
<syntaxhighlight lang="php">
foreach($stmt as $row)
{
    printf("%s lives in %s<br />\n", $row['name'], $row['city']);
}
// 0から始まるオフセットを取得することもできる
foreach($stmt as $i => $row)
{
    printf("[%d] %s lives in %s<br />\n", $i, $row['name'], $row['city']);
}
</syntaxhighlight>
<br>
==== PDOStatement::fetchObjectメソッド ====
連想配列の代わりにクラスオブジェクトを取得する。<br>
これは、<code>PDO::FETCH_OBJ</code>を指定して<code>PDOStatement::fetch</code>メソッドを使用する場合と同じであるが、こちらの方が簡潔に記述できる。<br>
<syntaxhighlight lang="php">
while($row = $stmt->fetchObject())
{
    printf("%s lives in %s<br />\n", $row->name, $row->city);
}
</syntaxhighlight>
<br>
==== PDOStatement::fetchColumnメソッド ====
特定の1カラムのみを文字列として取得する。<br>
これは、<code>PDO::FETCH_COLUMN</code>を指定して<code>PDOStatement::fetch</code>メソッドを使用する場合と同じでるが、こちらの方が簡潔に記述できる。<br>
<br>
先頭から数えてそのカラムが何番目(0オリジン)にあるかを第1引数として渡す。(省略する場合、0を指定したとみなされる)<br>
カラムの値に0が含まれる可能性がある場合は、<code>false !==</code>の判定をしなければならない。<br>
<syntaxhighlight lang="php">
while(false !== $value = $stmt->fetchColumn())
{
    echo "{$value}<br />\n";
}
</syntaxhighlight>
<br>
==== PDOStatement::fetchAllメソッド ====
全てのレコードを取得して2次元配列とする。<br>
* 引数を省略する場合、デフォルトフェッチモードが使用される。
* 特定のカラムのみ全てのレコードを取得して1次元配列とする場合、<br>第1引数に<code>PDO::FETCH_COLUMN</code>を指定して、第2引数に先頭から数えてそのカラムが何番目(0オリジン)にあるかを渡す。(省略する場合、0を指定したとみなされる)
<syntaxhighlight lang="php">
$rows = $stmt->fetchAll();
var_dump($rows);
$values = $stmt->fetchAll(PDO::FETCH_COLUMN);
var_dump($values);
</syntaxhighlight>
<br>
==== PDOStatement::setFetchModeメソッド ====
上記のセクションでは、<code>PDO</code>クラス自身にデフォルトフェッチモードを指定する方法を記載したが、<br>
<code>PDOStatement::setFetchMode</code>メソッドを使用する場合、個別に発行された<code>PDOStatement</code>クラスに対して、後からフェッチモードを指定できる。<br>
また、モードにより引数の渡し方が異なるため、詳細はPHPのマニュアルを参照すること。<br>
<br>
以下の例では、0番目のカラムをforeach文で取得している。<br>
<syntaxhighlight lang="php">
$stmt->setFetchMode(PDO::FETCH_COLUMN, 0);
foreach($stmt as $i => $name)
{
    // ...処理
}
</syntaxhighlight>
<br>
また、<code>PDO::query</code>メソッドも同様の形式でフェッチモードを指定することができる。<br>
以下の例では、上記の例と同様、0番目のカラムをforeach文で取得している。<br>
<syntaxhighlight lang="php">
foreach($pdo->query($sql, PDO::FETCH_COLUMN, 0) as $i => $name)
{
    // ...処理
}
</syntaxhighlight>
<br>
==== SQLインジェクション攻撃の対策 ====
以下の例では、フォームで入力したキーの値に応じたデータを取得している。<br>
ユーザの入力内容は自動的に引用符で括られるため、SQLインジェクション攻撃の恐れは無い。<br>
<syntaxhighlight lang="php">
<?php
    $stmt = $dbh->prepare("SELECT * FROM REGISTRY where name = ?");
    if($stmt->execute(array($_GET['name'])))
    {
      while($row = $stmt->fetch())
      {
          print_r($row);
      }
    }
?>
</syntaxhighlight>
<br><br>
== レコードの追加(プリペアドステートメント) ==
以下の例では、nameおよびvalueを<u>名前付きプレースホルダ</u>で置き換えて、INSERT文を実行している。<br>
<syntaxhighlight lang="php">
<?php
    $stmt = $dbh->prepare("INSERT INTO REGISTRY (name, value) VALUES (:name, :value)");
    $stmt->bindParam(':name', $name);
    $stmt->bindParam(':value', $value);
    // 1レコード目の挿入
    $name = 'one';
    $value = 1;
    $stmt->execute();
    // パラメータを変更して、2レコード目の挿入
    $name = 'two';
    $value = 2;
    $stmt->execute();
?>
</syntaxhighlight>
<br>
以下の例では、nameおよびvalueを<u>プレースホルダ<code>?</code></u>で置き換えて、INSERT文を実行している。<br>
<syntaxhighlight lang="php">
<?php
    $stmt = $dbh->prepare("INSERT INTO REGISTRY (name, value) VALUES (?, ?)");
    $stmt->bindParam(1, $name);
    $stmt->bindParam(2, $value);
    // 1レコード目の挿入
    $name = 'one';
    $value = 1;
    $stmt->execute();
    // パラメータを変更して、2レコード目の挿入
    $name = 'two';
    $value = 2;
    $stmt->execute();
?>
</syntaxhighlight>
<br><br>
== レコードの更新(プリペアドステートメント) ==
以下の例では、name、value、idを<u>名前付きプレースホルダ</u>で置き換えて、UPDATE文を実行している。<br>
<syntaxhighlight lang="php">
<?php
    $stmt = $dbh->prepare("UPDATE REGISTRY SET name = :name value = :value WHERE id = :id");
    $stmt->bindParam(':name', $name);
    $stmt->bindParam(':value', $value);
    $stmt->bindParam(':id', $id);
    // レコード目の更新
    $name = 'one';
    $value = 1;
    $stmt->execute();
?>
</syntaxhighlight>
<br>
以下の例では、name、value、idを<u>プレースホルダ<code>?</code></u>で置き換えて、UPDATE文を実行している。<br>
<syntaxhighlight lang="php">
<?php
    $stmt = $dbh->prepare("UPDATE REGISTRY SET name = ? value = ? WHERE id = ?");
    $stmt->bindParam(1, $name);
    $stmt->bindParam(2, $value);
    $stmt->bindParam(3, $id);
    // レコードの更新
    $name = 'one';
    $value = 1;
    $stmt->execute();
?>
</syntaxhighlight>
<br><br>
== ストアドプロシージャ ==
==== ストアドプロシージャの呼び出し : 出力パラメータの指定 ====
データベースドライバがサポートしている時、入力パラメータだけでなく、出力パラメータもバインドすることが可能である。<br>
<br>
出力パラメータは、ストアドプロシージャから値を取得するために使用する。<br>
この場合、返される値の大きさがどの程度になるのかをバインド時に知る必要がある。<br>
指定した大きさよりも大きな値が返される場合は、エラーが発生する。<br>
<syntaxhighlight lang="php">
<?php
    $stmt = $dbh->prepare("CALL sp_returns_string(?)");
    $stmt->bindParam(1, $return_value, PDO::PARAM_STR, 4000);
    // ストアドプロシージャを呼び出す
    $stmt->execute();
    print "プロシージャが返した値は $return_value です\n";
?>
</syntaxhighlight>
<br>
==== ストアドプロシージャの呼び出し : 入出力パラメータの指定 ====
入出力の両方に使用するパラメータを指定することもできる。<br>
入出力パラメータの書式は、出力パラメータと同じである。<br>
<br>
以下の例では、ストアドプロシージャに文字列'hello'を渡している。<br>
プロシージャの結果が返ってくると、文字列'hello'はプロシージャの返す値に置き換えられる。<br>
<syntaxhighlight lang="php">
<?php
    $stmt = $dbh->prepare("CALL sp_takes_string_returns_string(?)");
    $value = 'hello';
    $stmt->bindParam(1, $value, PDO::PARAM_STR|PDO::PARAM_INPUT_OUTPUT, 4000);
    // ストアドプロシージャの呼び出し
    $stmt->execute();
    print "プロシージャが返した値は $value です\n";
?>
</syntaxhighlight>
<br><br>
== プレースホルダの間違った使用例 ==
<syntaxhighlight lang="php">
<?php
    $stmt = $dbh->prepare("SELECT * FROM REGISTRY where name LIKE '%?%'");
    $stmt->execute(array($_GET['name']));
    // プレースホルダは、値全体に対して使用しなければなりません
    $stmt = $dbh->prepare("SELECT * FROM REGISTRY where name LIKE ?");
    $stmt->execute(array("%$_GET[name]%"));
?>
</syntaxhighlight>
<br><br>
{{#seo:
|title={{PAGENAME}} : Exploring Electronics and SUSE Linux | MochiuWiki
|keywords=MochiuWiki,Mochiu,Wiki,Mochiu Wiki,Electric Circuit,Electric,pcb,Mathematics,AVR,TI,STMicro,AVR,ATmega,MSP430,STM,Arduino,Xilinx,FPGA,Verilog,HDL,PinePhone,Pine Phone,Raspberry,Raspberry Pi,C,C++,C#,Qt,Qml,MFC,Shell,Bash,Zsh,Fish,SUSE,SLE,Suse Enterprise,Suse Linux,openSUSE,open SUSE,Leap,Linux,uCLnux,Podman,電気回路,電子回路,基板,プリント基板
|description={{PAGENAME}} - 電子回路とSUSE Linuxに関する情報 | This page is {{PAGENAME}} in our wiki about electronic circuits and SUSE Linux
|image=/resources/assets/MochiuLogo_Single_Blue.png
}}


__FORCETOC__
__FORCETOC__
[[カテゴリ:Web]]
[[カテゴリ:Web]][[カテゴリ:MySQL]]