PHP と MySQL と サーバサイド プリペアードステートメント
少し前のお話なのだけど、せっかくなので書いておくことにする。
MySQLのクエリログを見ていたら、 prepare に相当するログが一切流れていないことに気づいたので、PDO(mysql)でサーバサイドプリペアードステートメントが利用されていないんじゃないか疑惑が浮上。
php のバージョンは 5.2.11
mysql は 5.0 系
以下、検証してみた結果と、その対応です。
検証
簡単なソースコードを実行してみて、そのクエリログを見てみることにした。
PDO を利用
$p = new PDO('mysql:dbname=dbname;host=localhost', 'user', 'password'); $stmt = $p->prepare("SELECT * FROM table_name WHERE id=?"); $stmt->execute(array(1)); $stmt->execute(array(2));
クエリログ(PDO)
091016 16:59:20 227 Connect user@localhost on dbname 227 Query SELECT * FROM table_name WHERE id='1' 227 Query SELECT * FROM table_name WHERE id='2' 227 Quit
MySQLi を利用
$m = new mysqli('localhost', 'user', 'pass', 'dbname'); $stmt = $m->prepare("SELECT * FROM table_name WHERE id=?"); $stmt->bind_param('d', $id); $id=1; $stmt->execute(); $id=2; $stmt->execute();
クエリログ(MySQLi)
091016 16:58:28 228 Connect user@localhost on dbname 228 Prepare [1] SELECT * FROM table_name WHERE id=? 228 Execute [1] SELECT * FROM table_name WHERE id=1 228 Execute [1] SELECT * FROM table_name WHERE id=2 228 Quit
MySQLiでは利用されているprepareが PDOでは利用されていない事が分かる。
原因
5.2.11のソースコードより
ext/pdo_mysql/mysql_driver.c@158 から始まる ''mysql_handle_preparer'' が、mysql利用時の PDO::parepare の実体
158 static int mysql_handle_preparer(pdo_dbh_t *dbh, const char *sql, long sql_len, pdo_stmt_t *stmt, zval *driver_options TSRMLS_DC) 159 { 160 pdo_mysql_db_handle *H = (pdo_mysql_db_handle *)dbh->driver_data; (中略) 173 if (H->emulate_prepare) { 174 goto end; 175 } 176 177 #if HAVE_MYSQL_STMT_PREPARE 178 server_version = mysql_get_server_version(H->server); 179 if (server_version < 40100) { 180 goto fallback; 181 } 182 stmt->supports_placeholders = PDO_PLACEHOLDER_POSITIONAL; (中略) 236 fallback: 237 #endif 238 end: 239 stmt->supports_placeholders = PDO_PLACEHOLDER_NONE; 240 241 return 1;
ここで、''stmt->supports_placeholders'' に指定可能な定数は以下の通り
- PDO_PLACEHOLDER_NONE (0)
- DBはプレースホルダをサポートしない
=> PDOによるフルエミュレーション - PDO_PLACEHOLDER_NAMED (1)
- DBは名前付きプレースホルダのみサポートする
=> 「?」を利用する場合はエミュレーション - PDO_PLACEHOLDER_POSITIONAL (2)
- DBは序数プレースホルダのみサポートする
=> 「:xxx」を利用する場合はエミュレーション - PDO_PLACEHOLDER_NAMED | PDO_PLACEHOLDER_POSITIONAL (3)
- DBは、両方のプレースホルダをサポートする
=> エミュレーションは一切行われない
(参照: https://www.codeblog.org/blog/moriyoshi/20061221.html )
つまり、以下の全ての条件に一致することで、始めてサーバサイドプリペアードステートメント(序数プレースホルダのみ)が利用できる。
- 173行目より、dbh->driver_data->emulate_prepare が偽
- 177行目より、HAVE_MYSQL_STMT_PREPARE が真
- 179行目より、接続先のMySQLバージョンが4.01以上
逆に、上記条件のうち、一つでも満たさないとPDOによるフルエミュレーションとなる。
HAVE_MYSQL_STMT_PREPARE に関しては、RPMのログを見る限りでは、定義されていた。
checking for MySQL support for PDO... yes, shared checking for mysql_config... /usr/bin/mysql_config checking for mysql_query in -lmysqlclient... yes checking for mysql_commit... yes <strong>checking for mysql_stmt_prepare... yes</strong> # ← これ! checking for mysql_next_result... yes checking for mysql_sqlstate... yes
また、利用しているMySQLサーバはバージョン5.0系なので、当然 4.01以上。
ということで、 dbh->driver_data->emulate_prepare の存在が焦点となってくる。
ext/pdo_mysql/mysql_driver.c@434 から始まる ''pdo_mysql_handle_factory'' 関数が、mysql 利用のPDOオブジェクト構築の際に呼ばれるっぽい(参照: http://www.php.net/manual/ja/internals2.pdo.php )のだが、
その中で、
434 static int pdo_mysql_handle_factory(pdo_dbh_t *dbh, zval *driver_options TSRMLS_DC) /* {{{ */ 435 { 436 pdo_mysql_db_handle *H; (中略) 459 H = pecalloc(1, sizeof(pdo_mysql_db_handle), dbh->is_persistent); (中略) 472 dbh->driver_data = H; 473 H->max_buffer_size = 1024*1024; 474 H->buffered = H->emulate_prepare = 1;
という、初期化をしている
重要なのはここ
474 H->buffered = H->emulate_prepare = 1;
ということで、デフォルトではサーバサイドプリペアードステートメントを利用しないことが分かる。
対策
ソースコードより、
ext/pdo_mysql/mysql_driver.c の ''pdo_mysql_handle_factory'' 内で以下のように''driver_options'' が渡された場合に ''emulate_prepare'' を上書きする処理があるので、
PDOの構築時に、第4引数 ($driver_options)で、
- array(PDO::ATTR_EMULATE_PREPARES => false)
または
- array(PDO::MYSQL_ATTR_DIRECT_QUERY => false)
を渡してやればよい
477 if (driver_options) { (中略) 484 H->emulate_prepare = pdo_attr_lval(driver_options, 485 PDO_MYSQL_ATTR_DIRECT_QUERY, H->emulate_prepare TSRMLS_CC); 486 H->emulate_prepare = pdo_attr_lval(driver_options, 487 PDO_ATTR_EMULATE_PREPARES, H->emulate_prepare TSRMLS_CC);
ちなみに、pdo_attr_lval は、ext/pdo/php_pdo_driver.h@190 で定義されているインライン関数。
array型である options の option_name 要素があればその値を返し、無ければ TSRMLS_DC を返す
190 static inline long pdo_attr_lval(zval *options, enum pdo_attribute_type option_name, long defval TSRMLS_DC)
あるいは
ext/pdo_mysql/mysql_driver.c@304 の ''pdo_mysql_set_attribute'' (msql利用PDOにおける PDO::setAttribute の実体) において、
304 static int pdo_mysql_set_attribute(pdo_dbh_t *dbh, long attr, zval *val TSRMLS_DC) 305 { 306 switch (attr) { (中略) 321 case PDO_MYSQL_ATTR_DIRECT_QUERY: 322 case PDO_ATTR_EMULATE_PREPARES: 323 ((pdo_mysql_db_handle *)dbh->driver_data)->emulate_prepare = Z_BVAL_P(val);
とあるので、
PDO オブジェクト $pdo に対して、
- $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
あるいは
- $pdo->setAttribute(PDO::MYSQL_ATTR_DIRECT_QUERY, false);
を実行すればよい