Edited at

最近PDOを使っていてハマったこと2つ。

More than 5 years have passed since last update.

nicosearch っていうニコニコ大百科のデータをもとにニコニコ動画の検索欄とかありとあらゆる場所にサジェストを提供するサービスを共同で制作していて、僕はサーバサイドのスクリプト担当なんだけど、ニコニコ大百科のデータを収集するために定期的に大百科をスクレイピングしてデータをMySQLに突っ込む必要があって、そのクローラをPHPで書いていた。長時間(1週間とか、あるいは延々と)走らせ続けないと発生しない問題もあるので、あまりこういうことで困ることは少ないような気がします。

(そもそもそれPHPでやることじゃないのでは、と思った人はごもっともです。)


1. MySQLのwait_timeoutでコネクションが閉じちゃう

コネクションを張ってからの累計アイドル時間がwait_timeoutで設定した秒数に達するとコネクションが閉じられてしまう。これはソフトウェアが異常終了してコネクションが正しく閉じられなかった時とかの対策のために存在する。

最初wait_timeoutはアイドル時間だと思っていたんだけど、実は"累計"だったことが判明。nicosearchの環境では60秒とかなり短めに設定してあったので、割とすぐに(と言っても発現するのは6時間~3日くらい経ってから)発現した。これ1時間とかだったら原因の特定はまず無理だったと思う。多分1周間に一度、ヘタしたら1ヶ月に1度、そういうレベルになってただろうから(;^ω^)

同時にPDOの良くないところは、原因がなんだろうが、PDOはクエリの実行が失敗するとfalseを返してそれだけっていうところです。

で、原因を調べるためには、実行したあとにわざわざPDOStatement::errorInfo()っていうメソッドにアクセスしないといけない。

例外投げてよって話だよね。

$result = $stmt->execute();

var_dump($result); //=>string(5) false (例外投げてよね)


2.PDOのbindParam()でバインドした変数の型が文字列になる

PDOはプリペアドステートメントとプレースホルダがお決まりなんだけど、プレースホルダに値をバインドする方法はいくつかあって、実行時にexecute(array('hoge'))みたいな感じで値を渡してバインドしたり、bindParam()やbindValue()で実行する前に値をバインドする方法がある。

bindParam()は実は、実際に値を評価するのはexecute()の時。だから未定義の変数を使ったりできる。(でも、その場ではバインドしないから整数やリテラルは使えない。)

で、PDOの解説をしてる記事はやたらめったらbindParam()を使ってるんだけど、bindParam()にはちょっとアレなところがあって、execute()時に評価するために内部で参照として持ってて、どうも評価時、プレースホルダにバインドする処理を通すことで評価した変数の型がすべて文字列型になってしまうという副作用があるみたい。

$stmt->bindParam(1, $int, PDO::PARAM_INT);

var_dump($int); //=> int(10)
$stmt->execute();
var_dump($int); //=> string(2) "10" ←文字列になってる!

PHPのリファレンスのユーザノートにも、「おいこんな挙動になるぞ」っていう報告がありました。

ですが「bindValue()とbindParam()の違い」とかいうタイトルの記事には以外にもこういう解説がされていることは少ないです。ループの中でbindParam()をしている場合、2回目からクエリが失敗する、という不可解な問題に悩まされると思います。

$stmt = $pdo->prepare('SELECT * FROM hoge WHERE 1 LIMIT ? ,30');

//一度目のexecute()で$intが文字列型になるので、2度めから動かなくなる
for($i = 0; $i < 10; $i++) {
$stmt->bindParam(1, $int, PDO::PARAM_INT);
$stmt->execute();
}

僕がハマった2つのポイントは、場合によっては全く出くわさない可能性があります。

1つ目は長時間走らせないと出てこない。

2つ目も、MySQLはSELECTやUPDATE句で整数型を文字列としてプレースホルダにバインド('10'みたいに)しても一応動くので、LIMIT句とか整数をエスケープしちゃうと動かなくなっちゃうような場合にのみ発生するなど、かなり限定的。(それに、ループの中でbindParam()とかしている状況じゃないと、気づかないかも。)