こんにちは、バックエンドエンジニアの kaz です。
今回「 stack overflow 」(スタック・オーバーフロー)で議論されているなかから選んだ質問がこちら。
-
Why shouldn’t I use mysql_* functions in PHP?
なんでPHPの mysql_* 関数は使っちゃいけないの?
今回の質問では、よく「仕様を避けるべき」とされるレガシーな mysql_*
系関数に対して、「なんで使うべきじゃない?」という疑問を投げかけられました。これらは使うべきでないとはいえ、昔ながらのコードを引き継いでずっと使い続けられているケースもあります。これはどうすべきなんでしょうか?
それでは、この疑問を解決する模範解答を見ていきましょう!
目次
[Question] Madara Uchihaさんによる質問
質問者 | Madara Uchiha さん |
---|
mysql_* 系関数を使うべきではないとされている技術的な理由ってなんですか?(たとえば mysql_query()
とか mysql_connect()
、 mysql_real_escape_string()
など)
すでにサイト上で使ったコードが動いているのに、なぜ他のものを使わなければいけないんでしょうか?
[Answer 1]Quentinさんによる回答
解答者 | Quentin さん ※改定一覧は こちら |
---|
こちらは最も票を得た回答です。端的にして明快に mysql_*
系関数の欠点を挙げられました。
解答者による解説
MySQL エクステンションは、
- アクティブに開発が行われていません
- PHP 5.5 (2013年6月リリース)から 公式に 非推奨 とされました
- PHP 7.0 (2015年12月リリース)から 完全に 削除 されました
- オブジェクト指向インターフェイスが存在しません
- 以下をサポートしていません
- ノンブロッキングな、非同期クエリー
- プリペアドステートメント
- ストアドプロシージャ
- 複数ステートメント
- トランザクション
- “新しい” パスワード認証方式( MySQL 5.6 でデフォルトとなり 5.7 で必須化されました)
- MySQL 5.1のすべての機能
非推奨化された以上、これを使い続けることはあなたのコードの将来性に限界をもたらします。
プリペアドステートメントに対応していないことは好ましくなく、これによってもたらされる「クエリのエスケープ処理コードを減らした、クリーンで問題の起きにくいコード」という恩恵を受けられず、すべて個別の関数を用いて手動で処理する必要が出てきます。
SQLエクステンションの比較 もご参照ください。
[Answer 2]NullPoiиteяさんによる回答
解答者 | NullPoiиteя さん ※改定一覧は こちら |
---|
こちらの回答は得票数こそ次点でしたが、あらゆる欠点に関する理由を網羅し、代替案のチュートリアルまで加えられた非常に濃厚な内容となっています。その内容の濃さたるや「 stack overflow で見たどんな解答よりも完璧で最高だ」というコメントが付いていたほど。
解答者による解説
PHP は、MySQL に接続するために 3 つの異なる API「mysql
」( PHP 7 で削除)「 mysqli
」「 PDO
」を提供しています。
mysql_*
関数はかつてとてもよく使われていましたが、今ではその利用は推奨されなくなりました。ドキュメント作成チームがデータベースのセキュリティーについて議論を行った結果、一般的に使われていた ext/mysql エクステンションの利用を今後避けるようユーザーに伝えていくことになった……というのがその背景です。(php.internals: deprecating ext/mysql を参照)
その後、PHP の開発チームは、ユーザーが mysql_connect()
や mysql_pconnect()
、または ext/mysql
に組み込まれた暗黙的な接続機構を用いて MySQL に接続したときに E_DEPRECATED
警告を出力してユーザーに警告することにしました。
ext/mysql
は PHP 5.5 以後公式に非推奨となり、そしてPHP 7 で削除されました。
赤色のボックスを見かけましたか?
PHP マニュアルで mysql_*
系関数のページを見ると、「これらの関数は利用すべきではありません」という旨の赤色のボックスが表示されます。
この対応が求められる理由は
ext/mysql
を避ける理由はセキュリティーだけではなく、MySQL データベースのすべての機能を利用できるようにするためでもあります。
そもそも ext/mysql
は MySQL 3.23 向けに作られたもので、その後もこの古いバージョンと互換性を保つことを目的に追加された機能はごくわずかに留まるという状況になっていました。ext/mysql
でサポートされていない不足機能としては次のものが挙げられます。(PHP マニュアルより)
- ストアドプロシージャ(複数の結果セットを扱えない)
- プリペアドステートメント
- 暗号化( SSL )
- 圧縮
- すべての文字コードのサポート
mysql_* 関数を使わない理由
- 積極的な開発が行われていない
- PHP 7 で削除された
- オブジェクト指向のインターフェイスの不足
- ノンブロッキングな非同期クエリーに非対応
- プリペアドステートメントやパラメタ化クエリに非対応
- ストアドプロシージャに非対応
- 複数ステートメントに非対応
- トランザクションに非対応
- MySQL 5.1 の全機能に対応していない
このなかでもとりわけプリペアドステートメントの対応については、これを用いることで個別の関数呼び出しによって手動で外部データをエスケープしたりエラーを検証していたコードを削減できるため、特に重要になります。
SQL エクステンションの比較もご参照ください。
非推奨警告の抑制
コードを MySQLi
や PDO
に書き換えるまでのあいだ、 php.ini で error_reporting
の指定を書き換えることで E_DEPRECATED
エラーを出力しないようにできます。
error_reporting = E_ALL ^ E_DEPRECATED
ただし、注意点としてこの設定は MySQL 以外の すべてのDEPRECATEDエラー も非表示にしてしまいます。(PHP マニュアルより)
適切なドライバーを選ぶには Dejan Marjanovic 氏の記事が参考になります: PDO vs. MySQLi: Which Should You Use?
このうちより優れているドライバーは PDO
なので、以下に PDO
のシンプルなチュートリアルを記載します。
単純端的な PDO チュートリアル
とりあえず疑問なのは……そもそも PDO
ってなに?
PDO ( PHP Data Objects ) とは、統一された方法でさまざまなデータベースにアクセスできるようにしてくれるデータベースアクセスレイヤーです。
MySQL への接続
まず mysql_*
を使った方法は次のとおり。PHP5.5 から Deprecated とされた「古い手法」です。
$link = mysql_connect('localhost', 'user', 'pass');
mysql_select_db('testdb', $link);
mysql_set_charset('UTF-8', $link);
PDO を使った方法
やることはただひとつ、新しい PDO
オブジェクトを作りましょう。コンストラクタはデータソースを指定するための引数を受け取ります。PDO
のコンストラクタはたいてい 4 つの引数としてまず DSN
(データソース名)、また任意で username
と password
を受け取ります。(訳注: 3 つしかないのは原文ママ)
ここで出てきた引数は DSN
というのを除けば説明不要でしょう; この DSN
は PDO
で新しく追加されたもので、基本的には PDO
に使用するドライバと接続情報を伝えるための設定値の文字列となっています。詳細は PDO MySQL DSN をご参照ください。
$db = new PDO('mysql:host=localhost;dbname=testdb;charset=utf8', 'username', 'password');
注意: ここで charset=UTF-8
と書くこともできますが、これは稀にエラーを起こすことがあるので utf8
と書くのを推奨します。
もし何らかの接続エラーが発生した場合は、エラー処理を行えるように PDO はキャッチ可能な PDOException
例外を投げます。
- 読むといいもの
- 接続とコネクション管理
第 4 引数に配列を渡すことで、追加でいくつかのドライバーオプションを渡すことができます。このなかでオススメなのは、PDO
を例外モードで動作させるオプションです。またいくつかの PDO
ドライバーではネイティブでプリペアドステートメントをサポートしていないものがあり、その場合 PDO
はプリペア処理をエミュレートしようとしますが、この処理を手動で有効できます。もちろんサーバーサイドでネイティブなプリペアドステートメントを行いたい場合は、明示的にこのオプションを false
としましょう。
また MySQL
ドライバーはプリペアエミュレーションを標準で有効にしようとしますが、 PDO
を安全に使うためにはこのエミュレーションを明示的に無効化するのが好ましいですね。
これは古いバージョンの MySQL
(もちろん推奨しません)を用いている場合にのみ影響します。
PDO の初期化後にアトリビュートを上書きできる?
可能です。
次に使い方の例を示します。
$db = new PDO('mysql:host=localhost;dbname=testdb;charset=UTF-8',
'username',
'password',
array(PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
PDO の初期化後にアトリビュートを上書きできる?
setAttribute
メソッドを用いることで、いくつかのアトリビュートは PDO を初期化した後でも設定することができます:
$db = new PDO('mysql:host=localhost;dbname=testdb;charset=UTF-8',
'username',
'password');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
エラー処理
PDO
でのエラー処理は mysql_*
に比べるとはるかに簡単です。
mysql_* を用いた場合の一般的な処理方法
//Connected to MySQL
$result = mysql_query("SELECT * FROM table", $link) or die(mysql_error($link));
OR die()
では die
に渡された値を扱えないため、これはエラーを処理するための最適な方法とは言えません。これは単にスクリプトを突然停止させて画面にエラー内容を表示するものですが、そもそもエラー内容など本来ユーザーに見せるべきものではなく、これでは悪いハッカーにスキーマ情報を暴露してしまいかねません。その代わりに mysql_*
関数の返り値はよく mysql_error() と併せることでエラー処理が行われます。
これに対し PDO
はよりよい解決策として「例外」を提供しています。適切なエラー処理を行うには、 PDO
を用いたあらゆる処理を try
–catch
ブロックで囲いましょう。 PDO のエラーモードは 3 種類あり、アトリビュートを使うことでこれを強制的に指定することが可能です。利用可能なエラー処理モードは次のとおりです。
PDO::ERRMODE_SILENT
は単純にエラーコードのみをセットするもので、mysql_*
とほとんど同じように動作します。エラーの詳細を得るには$db->errorInfo();
を使わなければなりません。PDO::ERRMODE_WARNING
はE_WARNING
警告を出力します。Fatal ではないランタイムエラーのため、スクリプトの実行は中断されません。PDO::ERRMODE_EXCEPTION
は例外を発生させます。この例外PDOException
はPDOであることを示すものであり、独自のコードでPDOException
を投げるべきではありません。PHP の例外に関する詳細は Exceptions をご参照ください。例外はキャッチされなかった場合にor die(mysql_error());
と非常によく似た働きをします。しかしor die()
と異なり、PDOException
例外はキャッチすることができ、これを適切に処理することが可能です。
- オススメの記事
参照例
$stmt->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
$stmt->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING );
$stmt->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
これらは次のように try
–catch
で囲って利用できます。
try {
//Connect as appropriate as above
$db->query('hi'); //Invalid query!
}
catch (PDOException $ex) {
echo "An Error occured!"; //User friendly message/message you want to show to user
some_logging_function($ex->getMessage());
}
ただし、直ちに try
–catch
を使ってエラー処理しなければならないわけではありません。妥当なタイミングでキャッチしてやればいいのですが、いずれにせよ try
–catch
を使うことは強く推奨します。場合によっては PDO
を呼んでいる関数の外でキャッチすることもあります。
function data_fun($db) {
$stmt = $db->query("SELECT * FROM table");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
//Then later
try {
data_fun($db);
}
catch(PDOException $ex) {
//Here you can handle error and show message/perform action you want.
}
あるいは別の方法として、or die()
を使って mysql_*
のように処理したりするなども可能ですが、この使いみちは多岐に渡ります。display_errors off
を指定することで本番環境でエラー表示を隠しつつ、エラーログを読むこともできます。
さて、ここまで呼んだ皆様はおそらくこう感じられたでしょう。「とりあえず、手始めに SELECT
とか INSERT
とか UPDATE
とか DELETE
ステートメントの使い方を勉強したりしてみたいだけだよ! 何だよこれ!」と。 ご心配なさらず、まずはやってみましょう。
データの取得
mysql_*
でこんなことをやろうとしていたとします。
<?php
$result = mysql_query('SELECT * from table') or die(mysql_error());
$num_rows = mysql_num_rows($result);
while($row = mysql_fetch_assoc($result)) {
echo $row['field1'];
}
これが PDO
を使うとこのようになります。
<?php
$stmt = $db->query('SELECT * FROM table');
while($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
echo $row['field1'];
}
あるいはこのような感じです。
<?php
$stmt = $db->query('SELECT * FROM table');
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
//Use $results
注意: もし次のようなメソッド(query()
)を利用している場合、この返り値は PDOStatement
オブジェクトになります。そのため結果を取得したい場合は、上記のように利用してください。
<?php
foreach($db->query('SELECT * FROM table') as $row) {
echo $row['field1'];
}
PDO におけるデータはステートメントハンドルの ->fetch()
メソッドを利用して取得します。これを用いる最適な方法として、fetch を呼ぶ前に PDO にどのようにデータを取得したいかを指定しましょう。次のセクションでこの説明を行います。
取得モード
上記の例において、fetch()
および fetchAll()
の引数で PDO::FETCH_ASSOC
を用いてることに着目してください。これは PDO
に結果セットとしてフィールド名をキーとする連想配列を返すよう指定するものです。他にも複数の取得モードが用意されていますので、それらを一つ一つを紹介していきます。
まずはじめに、どのように取得モードを選ぶべきかを説明します。
$stmt->fetch(PDO::FETCH_ASSOC)
この例では、引き続き fetch()
を利用しています。この他にも次のものが利用できます。
PDOStatement::fetchAll() |
結果セットのすべての行を含んだ配列を返します。 |
---|---|
PDOStatement::fetchColumn() |
結果セットの次の行から、一列だけを取得して返します。 |
PDOStatement::fetchObject() |
結果セットの次の行を取得し、オブジェクトとして返します。 |
PDOStatement::setFetchMode() |
このステートメントに対してデフォルトの取得モードを設定します。 |
それでは、取得モードの説明をしましょう。
PDO::FETCH_ASSOC |
結果セットのカラム名をインデックスとする配列を返します。 |
---|---|
PDO::FETCH_BOTH (デフォルト) |
結果セットのカラム名およびカラム順の連番の両方をキーとする配列を返します。 |
この他にもさらに選択肢はあります!全てのフェッチモードは PDOStatement
Fetch documentation をご参照ください。
結果の件数を取得するには、 mysql_num_rows
の代わりに PDOStatement
を使って次のように rowCount()
を実行します。
<?php
$stmt = $db->query('SELECT * FROM table');
$row_count = $stmt->rowCount();
echo $row_count.' rows selected';
最後に挿入された ID を取得します。
<?php
$result = $db->exec("INSERT INTO table(firstname, lastname) VAULES('John', 'Doe')");
$insertId = $db->lastInsertId();
挿入、更新、削除ステートメント
mysql_*
関数ではこのように処理していました。
<?php
$results = mysql_query("UPDATE table SET field='value'") or die(mysql_error());
echo mysql_affected_rows($result);
PDO ではこれを次のように書きます。
<?php
$affected_rows = $db->exec("UPDATE table SET field='value'");
echo $affected_rows;
上記の PDO::exec
が SQL ステートメントを実行し、影響したレコードの行数を返しています。
挿入 および 削除については後述します。
上記のメソッドは、クエリ内で変数を使っていない場合にのみその利便性を発揮します。もし変数を使いたい場合は、上記のようなコードは絶対に記述せず、代わりに プリペアドステートメント をご利用ください。
プリペアドステートメント
Q. プリペアドステートメントってなに?なんで必要なの?
A. プリペアドステートメントは事前にコンパイルされたSQLステートメントで、サーバーにデータだけを送信し複数回実行することができるものです。
典型的なプリペアドステートメントの用法は下記のとおりです (Wikipediaより引用):
-
1. プリペア
アプリケーションによってステートメントテンプレートが作成され、データベースサーバー( DBMS )に送られます。具体的なデータ値は「パラメーター」とか「プレースホルダー」、「バインド変数」と呼ばれる形で未定義のまま留められます。(下記の例で?
とラベル付けされたもの)
INSERT INTO PRODUCT (name, price) VALUES (?, ?)
2. コンパイル
DBMS はこのステートメントテンプレートをパースしてコンパイル、そしてクエリの最適化を行い、その結果を保存します。ステートメントはまだ実行しません。3. 実行
その後、アプリケーションからパラメーターの値を渡し(バインドし)、DBMS がステートメントを実行します。アプリケーションから改めて異なるパラメーター値を渡し、このステートメントを複数回実行することもできます。この例ではたとえば第一パラメーターに ‘Bread’ 、第二パラメーターに1.00
などを渡すことが考えられます。
プリペアドステートメントは SQL クエリにプレースホルダーを含めることで利用できます。 基本的には「プレースホルダのないクエリ(この場合はパラメーターを与えないでください)」「無名プレースホルダーのあるクエリ」「名前付きプレースホルダーのあるクエリ」があります。
Q. 名前付きパラメーターってそもそもなに?どうやって使えばいいの?
A. 名前付きパラメーター、これは疑問符 (?
) の代わりにコロンとなにか値を意味する名前を書いて使います。名前付きパラメーターでは値の位置とか順番を気にする必要はありません。
$stmt->bindParam(':bla', $bla);
bindParam(parameter,variable,data_type,length,driver_options)
実行時に配列を渡すことで値をバインドすることも可能です。
<?php
$stmt = $db->prepare("SELECT * FROM table WHERE id=:id AND name=:name");
$stmt->execute(array(':name' => $name, ':id' => $id));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
他にもオブジェクト指向フレンズのみんなにとってナイスな機能として、名前付きプレースホルダーはオブジェクトを直接データベースに挿入できる機能があります。この場合はオブジェクトのプロパティーが名前付きフィールドとして解釈されます。例:
class person {
public $name;
public $add;
function __construct($a,$b) {
$this->name = $a;
$this->add = $b;
}
}
$demo = new person('john','29 bla district');
$stmt = $db->prepare("INSERT INTO table (name, add) value (:name, :add)");
$stmt->execute((array)$demo);
Q. 無名プレースホルダーってのはなんなの?どうやって使うの?
A. 例をお見せしましょう。
<?php
$stmt = $db->prepare("INSERT INTO folks (name, add) values (?, ?)");
$stmt->bindValue(1, $name, PDO::PARAM_STR);
$stmt->bindValue(2, $add, PDO::PARAM_STR);
$stmt->execute();
もう一つ。
$stmt = $db->prepare("INSERT INTO folks (name, add) values (?, ?)");
$stmt->execute(array('john', '29 bla district'));
この例では、プレースホルダとして名前の代わりに ?
と書かれているのがわかります。最初の例ではさまざまなプレースホルダに対して変数を割り当てています ($stmt->bindValue(1, $name, PDO::PARAM_STR);
)。そしてこれらのプレースホルダに値を割り当て、ステートメントを実行しています。次の例では一つめの配列要素が一つめの ?
に割り当てられ、二つめの値が二つめの ?
に割り当てられます。
▼注意
無名プレースホルダ では PDOStatement::execute()
に渡す配列の要素の並びに気をつける必要があります。
SELECT
, INSERT
, UPDATE
, DELETE
プリペアドクエリー
-
SELECT
:$stmt = $db->prepare(“SELECT * FROM table WHERE id=:id AND name=:name”);
$stmt->execute(array(‘:name’ => $name, ‘:id’ => $id));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); -
INSERT
:$stmt = $db->prepare(“INSERT INTO table(field1,field2) VALUES(:field1,:field2)”);
$stmt->execute(array(‘:field1’ => $field1, ‘:field2’ => $field2));
$affected_rows = $stmt->rowCount();
-
DELETE
:$stmt = $db->prepare(“DELETE FROM table WHERE id=:id”);
$stmt->bindValue(‘:id’, $id, PDO::PARAM_STR);
$stmt->execute();
$affected_rows = $stmt->rowCount(); -
UPDATE
:$stmt = $db->prepare(“UPDATE table SET name=? WHERE id=?”);
$stmt->execute(array($name, $id));
$affected_rows = $stmt->rowCount();
注意しておきたい点
たとえ PDO
または MySQLi
であっても、「完璧に安全」ではありません。ircmaxell 氏のこちらの回答をご確認ください。「 PDO のプリペアドステートメントは SQL インジェクションを十分防げますか? 」。彼の回答の一部をここに引用します。
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES GBK');
$stmt = $pdo->prepare("SELECT * FROM test WHERE name = ? LIMIT 1");
$stmt->execute(array(chr(0xbf) . chr(0x27) . " OR 1=1 /*"));
スタック・オーバーフローの最新情報を翻訳掲載!
主にプログラミング技術に関するナレッジを共有するコミュニティサイト「Stack Overflow」。2014 年に開設された日本語版サイトを利用しているエンジニアもいらっしゃることでしょう。ここでは、LIG のエンジニアが気になった本国版の投稿のなかから、プログラマーの悩みや課題を解消できるようなネタをピックアップし、翻訳して解説するコンテンツを手がけていきます。
>> スタック・オーバーフロー 本国サイト
>> スタック・オーバーフロー 日本語版サイト
LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。