PDOの利用

トップ > PHP入門 > PDOの利用

目次

広告

PDOとは

PDO(PHP Data Object)とは、PHP標準(5.1.0以降)のデータベース接続クラスのことです。

PHPは標準でMySQLやPostgreSQLやSQLiteなど、色々なデータベースに接続するための命令が用意されています。データベースの種類によって条件分岐させて命令を呼び出せば、プログラムを複数のデータベースに対応させることもできます。
ですがPDOを使用していれば、同じ命令で複数のデータベースに接続ができるようになるので、さらに開発が容易になります。

なお、PDOはPEAR::DBよりも高速に動作するため、「PEAR::DBを利用したプログラムに機能追加する」「PDOの設定がされていない環境で作成する」のような特別な理由がなければ、PDOを利用する方がいいでしょう。

PDOの利用方法

PDOは現在はPHP標準の機能なので、専用のソフトを追加でインストールする必要はありません。ですがもしPDOを利用できなければ、設定ファイル(php.ini)を編集する必要があります。

C:\xampp\php\php.ini の950行目あたりある

extension=php_pdo.dll
extension=php_pdo_mysql_libmysql.dll
extension=php_pdo_sqlite.dll

これらがPDOに関する設定部分です。この部分の行頭に ; があればPDOが無効になっているという意味なので、; を3つとも削除します。その後、Apacheを再起動してください。

PDOでのデータベース操作

以下はPDOでMySQLを使用したサンプルプログラムです。アドレス帳のデータを保存したテーブルの内容を、順に表示しています。(テーブルはMySQLの基本的な操作で作成した address テーブルです。)

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>サンプル</title>
</head>
<body>
<?php

try {
  $pdo = new PDO('mysql:dbname=phpdb;host=127.0.0.1', 'root', '1234');
} catch (PDOException $e) {
  exit('データベースに接続できませんでした。' . $e->getMessage());
}

$stmt = $pdo->query('SET NAMES utf8');
if (!$stmt) {
  $info = $pdo->errorInfo();

  exit($info[2]);
}

$stmt = $pdo->query('SELECT * FROM address WHERE no >= 10 AND no <= 20');
if (!$stmt) {
  $info = $pdo->errorInfo();

  exit($info[2]);
}

while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) {
  echo '<p>' . $data['no'] . ':' . $data['name'] . "</p>\n";
}

$pdo = null;

?>
</body>
</html>

次から、それぞれの処理を詳しく見ていきます。

データベースを操作するためのメソッド

PHPからMySQLを操作する際には mysql_connectmysql_query といった関数が用意されていましたが、これらはMySQL専用の関数です。PostgreSQLやSQLiteを操作するためには、別途専用の関数を使用します。
ですがPDOなら、同じ命令で色々なデータベースに接続することができます。

以下はMySQL用の関数との比較です。

処理内容 MySQL操作関数 PDO
MySQLに接続&データベース選択 mysql_connectmysql_select_db PDO
SQL実行 mysql_querymysql_fetch_array queryfetch
接続を切断 mysql_close PDO の戻り値に null を代入

データベースに接続する

PDOでデータベースに接続するには、PDO クラスを使用します。接続に成功するとオブジェクトが返されます。

オブジェクト = new PDO(
  'データベースの種類:dbname=データベース名;host=接続先アドレス', 'ユーザー名', 'パスワード'
);

MySQLに接続する場合、データベースの種類は mysql を指定します。もしPostgreSQLに接続したければ pgsql、SQLiteに接続したければ sqlite を指定します。他にも、色々な種類のデータベースに接続することができます。

接続に失敗するとPDOは「例外」を発生させます。これは trycatch を使用すれば補足することができます。具体的には

try {
  チェックしたい処理
} catch (PDOException $e) {
  例外が発生したときの処理
}

という形式になります。また、例外の情報は PDOException に続けて書いた変数に格納されます。(今回の場合は $e)格納されたエラーメッセージを表示したい場合は

$e->getMessage()

とします。この部分はPDOクラスの使い方と合わせて、決まり文句として書いておきましょう。

SQLを実行する

PDOで実際にデータベースにコマンドを送るには query メソッドを使用します。引数には実行したいコマンドを指定します。

オブジェクト = $pdo->query('実行するSQL文');

返されるオブジェクトはPDOStatementと呼ばれるオブジェクトで、$pdo の内容とは別のものですので注意してください。

コマンドが正しく実行できなかった場合、オブジェクトではなく NULL が返されます。また、$pdo->errorInfo() でエラーの内容が参照できるようになります。エラーの内容は配列で返され、先頭から順に「SQLのエラーコード」「ドライバ固有のエラーコード」「ドライバ固有のエラーメッセージ」が格納されます。ですので、

if (!オブジェクト) {
  $info = $pdo->errorInfo();

  exit($info[2]);
}

このようにすると、エラーが発生した際にエラーメッセージを表示することができます。

最初に文字コードを指定するSQLを実行しています。SET NAMES に続けて文字コードを指定すると、データベースで扱う文字コードを明示できます。(SET NAMES を使わなくても文字化けしない場合、この処理は不要です。)

レコードを取得するコマンドを指定した場合、PDOStatementオブジェクトに対して fetch メソッドを使用すれば、レコードを一件ずつ取得する事ができます。引数に PDO::FETCH_ASSOC を指定すると、連想配列の形式で取得します

オブジェクト = $stmt->fetch(PDO::FETCH_ASSOC);

取得したレコードすべてを順に取得して表示する場合、while と組み合わせて以下のように書きます。

while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) {
  echo '<p>' . $data['no'] . ':' . $data['name'] . "</p>\n";
}

これで「データを1件取り出すことができれば」という条件で繰り返し処理を行うため、データが存在する間はずっと echo が実行されます。

また、PDO::FETCH_ASSOC の代わりに PDO::FETCH_NUM を使用すれば、レコードを一件ずつ配列の形式で取得する事ができます。具体的には以下のように使用します。

while ($data = $stmt->fetch(PDO::FETCH_NUM)) {
  echo '<p>' . $data[0] . ':' . $data[1] . "</p>\n";
}

$data[0] には先頭の列である no が、$data[1] には次の列である name が格納されますので、実行結果は同じです。

データベースとの接続を切断する

PDOでデータベースとの接続を切断するには、PDO オブジェクトに null を代入します。

プレースホルダの利用

query メソッドを実行すると、与えられたSQL文を解析し、その後実行します。ですが検索条件のみ変化するSQL文を何度も実行するような場合、毎回SQL文全体を解析するのは無駄が多いです。
このような場合はプレースホルダを利用すれば、SQL文の解析は最初に一度だけ行い、その後は変化する部分のみを解析&実行することができます。

以下はプレースホルダを使用したサンプルプログラムです。

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>サンプル</title>
</head>
<body>
<?php

try {
  $pdo = new PDO('mysql:dbname=phpdb;host=127.0.0.1', 'root', '1234');
} catch (PDOException $e) {
  exit('データベースに接続できませんでした。' . $e->getMessage());
}

$stmt = $pdo->query('SET NAMES utf8');
if (!$stmt) {
  $info = $pdo->errorInfo();

  exit($info[2]);
}

$stmt = $pdo->prepare('SELECT * FROM address WHERE no >= :number1 AND no <= :number2');
$stmt->bindValue(':number1', 1, PDO::PARAM_INT);
$stmt->bindValue(':number2', 5, PDO::PARAM_INT);
$flag = $stmt->execute();
if (!$flag) {
  $info = $stmt->errorInfo();
  exit($info[2]);
}

while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) {
  echo '<p>' . $data['no'] . ':' . $data['name'] . "</p>\n";
}

$pdo = null;

?>
</body>
</html>

プレースホルダを利用しているのは以下の部分です。

$stmt = $pdo->prepare('SELECT * FROM address WHERE no >= :number1 AND no <= :number2');

プレースホルダを利用する場合、SQL文の指定は query ではなく prepare で行います。また、後ほど値をセットしたい箇所は : に続けて半角英数字でキーワードを書いておきます。キーワードは自分で決めることができますが、変数名と同様、できるだけ「どういう値が格納されるか?」を考えて名前を付けた方が、後でプログラムを読み返したときに理解しやすくなります。

次に

$stmt->bindValue(':number1', 1, PDO::PARAM_INT);
$stmt->bindValue(':number2', 5, PDO::PARAM_INT);

という処理がありますが、このようにすると「キーワード :number1 には 1 を割り当てる」「キーワード :number2 には 5 を割り当てる」という指定になります。

さらに

$flag = $stmt->execute();

という処理がありますが、execute メソッドを呼び出すとSQLが実行されます。つまり今回の場合、SELECT * FROM address WHERE no >= 1 AND no <= 5 を指定したときと同じ結果が得られます。

このように、query メソッドだけですぐに実行するのではなく、

  1. あらかじめSQLを定義する
  2. 必要に応じて値をセットする
  3. 実行する

という段階を踏みます。これがプレースホルダの基本的な使い方です。

数値の指定

プレースホルダで数値を指定する場合、上の例のように PDO::PARAM_INT を指定しておきます。無くてもPDOが自動的に数値なのか文字なのかを判断して適切に処理してくれますが、この判断は現時点では完全ではありません。具体的には、データベースにMySQLを使用している際に

$stmt = $pdo->prepare('SELECT * FROM address LIMIT :max');
$stmt->bindValue(':max', 10);
$flag = $stmt->execute();

このように指定するとSQLの文法エラーとみなされてしまいます。この場合、

$stmt = $pdo->prepare('SELECT * FROM address LIMIT :max');
$stmt->bindValue(':max', 10, PDO::PARAM_INT);
$flag = $stmt->execute();

このように PDO::PARAM_INT を追加すると正しく動作します。書き忘れ防止のためにも、数値を指定する場合は常に PDO::PARAM_INT を指定しておくといいでしょう。

なお、これは文字列を指定する場合は不要です。つまり、文字列の場合は

$stmt = $pdo->prepare('SELECT * FROM address WHERE name = :name1 OR name = :name2');
$stmt->bindValue(':name1', '山田太郎');
$stmt->bindValue(':name2', '山田花子');
$flag = $stmt->execute();

このように指定します。

同じSQLを繰り返し実行する

前回の例は検索を1度実行するだけだったので、あまりプレースホルダを使用する意味はありません。ですが、似た処理を何度も実行する場合はプレースホルダが有効に働きます。

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>サンプル</title>
</head>
<body>
<?php

try {
  $pdo = new PDO('mysql:dbname=phpdb;host=127.0.0.1', 'root', '1234');
} catch (PDOException $e) {
  exit('データベースに接続できませんでした。' . $e->getMessage());
}

$stmt = $pdo->query('SET NAMES utf8');
if (!$stmt) {
  $info = $pdo->errorInfo();

  exit($info[2]);
}

$stmt = $pdo->prepare('SELECT * FROM address WHERE no >= :number1 AND no <= :number2');

echo "<p>noが1~5のデータを表示します。</p>\n";

$stmt->bindValue(':number1', 1, PDO::PARAM_INT);
$stmt->bindValue(':number2', 5, PDO::PARAM_INT);
$flag = $stmt->execute();
if (!$flag) {
	$info = $stmt->errorInfo();
	exit($info[2]);
}
while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) {
  echo '<p>' . $data['no'] . ':' . $data['name'] . "</p>\n";
}

echo "<p>noが6~10のデータを表示します。</p>\n";

$stmt->bindValue(':number1',  6, PDO::PARAM_INT);
$stmt->bindValue(':number2', 10, PDO::PARAM_INT);
$flag = $stmt->execute();
if (!$flag) {
	$info = $stmt->errorInfo();
	exit($info[2]);
}
while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) {
  echo '<p>' . $data['no'] . ':' . $data['name'] . "</p>\n";
}

echo "<p>noが11~15のデータを表示します。</p>\n";

$stmt->bindValue(':number1', 11, PDO::PARAM_INT);
$stmt->bindValue(':number2', 15, PDO::PARAM_INT);
$flag = $stmt->execute();
if (!$flag) {
	$info = $stmt->errorInfo();
	exit($info[2]);
}
while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) {
  echo '<p>' . $data['no'] . ':' . $data['name'] . "</p>\n";
}

$pdo = null;

?>
</body>
</html>

少し長いですが、同じような検索処理を3回繰り返しています。

実行するSQLは、以下の一箇所でのみ定義しています。

$stmt = $pdo->prepare('SELECT * FROM address WHERE no >= :number1 AND no <= :number2');

その後、bindValue で値をセットして execute で実行して結果を表示…という処理を3回行っています。

このように処理すると、データベースがSQLを一から解釈する必要がなくなるため、実行速度を早くすることができます。今回は検索処理を3回実行するだけなので大差はありませんが、「データベースに10万件のデータを一括登録する」のような処理の場合、実行速度に差が現れます。

不正なアクセスへの対策

以上のように、プレースホルダはSQLを効率よく実行するための仕組みなのですが、プログラムに対する攻撃の防止にもなります。例えば

$stmt = $pdo->query('DELETE FROM address WHERE no = ' . $_POST['no']);

このようなプログラムで、ユーザーに指定された番号のデータを削除するとします。($_POST['no'] は、フォームから入力した値が格納されます。)もしユーザーが 10 を指定した場合、

DELETE FROM address WHERE no = 10

が実行され、番号が10のデータのみ削除されます。ですがもしユーザーが 1 OR 1 = 1 という値を指定した場合、

DELETE FROM address WHERE no = 1 OR 1 = 1

が実行され、address テーブル内のデータがすべて削除されてしまいます。また、

$stmt = $pdo->query('SELECT * FROM address WHERE no = ' . $_POST['no']);

このようなプログラムで、ユーザーに指定された番号のデータを表示するとします。($_POST['no'] は、フォームから入力した値が格納されます。)もしユーザーが 10 を指定した場合、

SELECT * FROM address WHERE no = 10

が実行され、番号が10のデータが表示されます。ですがもしユーザーが ; DELETE FROM address という値を指定した場合、

SELECT * FROM address WHERE no = 10; DELETE FROM address

が実行され、やはり address テーブル内のデータがすべて削除されてしまいます。

これを防ぐには addslashes'" をエスケープしたり、intval 関数で強制的に数値に変換すれば防ぐことができます。ですが沢山のデータベース操作処理を書いていると、どこかで書き忘れのミスが発生する可能性があります。

この対策に、プレースホルダを使用していれば、

$stmt = $pdo->prepare('SELECT * FROM address WHERE no > :number1 AND no <= :number2');
$stmt->bindValue(':number1', 10, PDO::PARAM_INT);
$stmt->bindValue(':number2', 20, PDO::PARAM_INT);
$flag = $stmt->execute();

と指定するだけで対策を行ってくれます。設定された値によって、PDOが適切なエスケープ処理を自動で行います。

以上の理由から、ユーザーからの入力をもとにSQL文を作成する場合、常にプレースホルダを利用した方が良いでしょう。

エラーモードの変更

PDOでデータベースへの接続に失敗すると例外が発生しますが、接続時以外も常に例外でエラーを捕らえることができます。

常に例外でエラーを捕らえるには、PDO の4つ目の引数に

array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)

を指定します。4つ目の引数ではPDOの動作設定を指定できるのですが、上のように指定するとエラー発生時には常に例外が発生するようになります。具体的には、以下のようなコードになります。

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>サンプル</title>
</head>
<body>
<?php

try {
  $pdo = new PDO('mysql:dbname=phpdb;host=127.0.0.1', 'root', '1234', array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
  $pdo->query('SET NAMES utf8');

  $stmt = $pdo->query('SELECT * FROM address WHERE no >= 10 AND no <= 20');
  while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) {
    echo '<p>' . $data['no'] . ':' . $data['name'] . "</p>\n";
  }
} catch (PDOException $e) {
  exit($e->getMessage());
}

$pdo = null;

?>
</body>
</html>

先程まではSQL文を実行するたびにエラーをチェックしていましたが、例外を利用すれば問題が発生した時点で catch の後の処理に飛ぶようになります。

つまり、毎回エラーをチェックしなくてもいいので、プログラムをすっきりと書くことができます。