フリーPHPスクリプト配布サイト。
PDO(PHP Data Object)とは、PHP標準(5.1.0以降)のデータベース接続クラスのことです。
PHPは標準でMySQLやPostgreSQLやSQLiteなど、色々なデータベースに接続するための命令が用意されています。データベースの種類によって条件分岐させて命令を呼び出せば、プログラムを複数のデータベースに対応させることもできます。
ですがPDOを使用していれば、同じ命令で複数のデータベースに接続ができるようになるので、さらに開発が容易になります。
なお、PDOはPEAR::DBよりも高速に動作するため、「PEAR::DBを利用したプログラムに機能追加する」「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で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_connect
や mysql_query
といった関数が用意されていましたが、これらはMySQL専用の関数です。PostgreSQLやSQLiteを操作するためには、別途専用の関数を使用します。
ですがPDOなら、同じ命令で色々なデータベースに接続することができます。
以下はMySQL用の関数との比較です。
処理内容 | MySQL操作関数 | PDO |
---|---|---|
MySQLに接続&データベース選択 | mysql_connect ~ mysql_select_db |
PDO |
SQL実行 | mysql_query ~ mysql_fetch_array |
query ~ fetch |
接続を切断 | mysql_close |
PDO の戻り値に null を代入 |
PDOでデータベースに接続するには、PDO
クラスを使用します。接続に成功するとオブジェクトが返されます。
オブジェクト = new PDO(
'データベースの種類:dbname=データベース名;host=接続先アドレス', 'ユーザー名', 'パスワード'
);
MySQLに接続する場合、データベースの種類は mysql
を指定します。もしPostgreSQLに接続したければ pgsql
、SQLiteに接続したければ sqlite
を指定します。他にも、色々な種類のデータベースに接続することができます。
接続に失敗するとPDOは「例外」を発生させます。これは try
と catch
を使用すれば補足することができます。具体的には
try {
チェックしたい処理
} catch (PDOException $e) {
例外が発生したときの処理
}
という形式になります。また、例外の情報は PDOException
に続けて書いた変数に格納されます。(今回の場合は $e
)格納されたエラーメッセージを表示したい場合は
$e->getMessage()
とします。この部分はPDOクラスの使い方と合わせて、決まり文句として書いておきましょう。
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
メソッドだけですぐに実行するのではなく、
という段階を踏みます。これがプレースホルダの基本的な使い方です。
プレースホルダで数値を指定する場合、上の例のように 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();
このように指定します。
前回の例は検索を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
の後の処理に飛ぶようになります。
つまり、毎回エラーをチェックしなくてもいいので、プログラムをすっきりと書くことができます。