2016/10/18 20:22:59

なんでPHPはMySQLからのリザルトがint型のはずなのにstring型になってしまうん?

目次(クリックするとジャンプします)
  • 1:動的型言語の悩ましいところ
  • 2:string型になっちゃう…だってPDOなんだもの。
  • 3:そもそもMySQLから返ってくるときに文字列になっている説
  • 3.1:原因はPDOのエミュレートモード
  • 4:型変換を回避する
  • 4.1:ドライバーの確認
  • 4.2:型変換がされないようにエミュレートモードをOFFにしてみる
  • 5:補足・ハマったポイント
  • 5.1:PDO::ATTR_STRINGIFY_FETCHESは?
  • 5.2:MySQL側での型変換
  • 5.2.1:MySQL側の型変換の回避?
  • 5.2.2:型変換の挙動の把握
  • 5.2.3:いろいろな挙動を考慮する必要性
  • 5.3:直接executeにパラメータを渡す場合
  • 6:まとめ

動的型言語の悩ましいところ

PHPのような動的型付け言語には多くの場合、型変換が各所で行われます。

これはユーザが型を意識しなくてもプログラミングがしやすくなる反面、よく型変換を理解していないとえらい目にあったりするという弊害もあります。

PHPよく話題にあがる事例として「データベースのリザルトでint型がstring型になる」問題があります。

@MINOもこの挙動が不思議でしたが、フェッチした後にキャストしてしまえばそれほど困る問題ではないので対処しようとはしませんでした。

でもなんだか気味が悪いので原因を追求してみることに。

実に深淵でした。

string型になっちゃう…だってPDOなんだもの。

とりあえずstring型になってしまう挙動を確認してみます。

以下のようなテーブルをPDOをつかって取得してみます。

id(int) name(text) age(int)
1 ikariya 72
2 katou 73
3 takagi 83
4 nakamoto 72
5 shimura 66

コードはこちら。

得られたリザルトはこうなりました。

array(3) {
  ["id"]=>
  string(1) "1"
  ["name"]=>
  string(7) "ikariya"
  ["age"]=>
  string(2) "72"
}
array(3) {
  ["id"]=>
  string(1) "2"
  ["name"]=>
  string(5) "katou"
  ["age"]=>
  string(2) "73"
}
array(3) {
  ["id"]=>
  string(1) "3"
  ["name"]=>
  string(6) "takagi"
  ["age"]=>
  string(2) "83"
}
array(3) {
  ["id"]=>
  string(1) "4"
  ["name"]=>
  string(8) "nakamoto"
  ["age"]=>
  string(2) "74"
}
array(3) {
  ["id"]=>
  string(1) "5"
  ["name"]=>
  string(7) "shimura"
  ["age"]=>
  string(2) "66"
}

確かに全部string型になっていますね…。

idageはMySQL上ではint型コラムなので、どこかで型変換がされているようです。

そもそもMySQLから返ってくるときに文字列になっている説

実のところ型変換が行われるのはPHP本体ではなくPDOの挙動のようです。

どうやらデフォルトモードのPDOでMySQLに接続している場合、int型でも(他の型でも)string型で返ってくるという挙動があり、これは仕様のようです。

質問サイトで同じような質問がありました。

teratail – MySQLのint型カラムよりPDOのfetch()で取得した変数の型がintではなくstringになるのですが、なぜでしょうか?

原因はPDOのエミュレートモード

具体的にはPDOがプリペアードステートメントエミュレートモードで動いている場合にint型がstring型になるようです。

PDOはデフォルトでプリペアードステートメントエミュレートモードというモードで動いています(はず)。

PHPマニュアルには以下のようなことが書いてありマス。

PDO::ATTR_EMULATE_PREPARES プリペアドステートメントのエミュレーションを有効または無効にする。 ドライバによってはネイティブのプリペアドステートメントをサポートしていなかったり 完全には対応していなかったりするものがある。この設定を使うと、常に プリペアドステートメントをエミュレートする (TRUE の場合) か、 ネイティブのプリペアドステートメントを使おうとする (FALSE の場合) かを設定できる。現在のクエリを正しく準備できなかった場合は、常にエミュレート方式を使う。 bool で指定する。

PDO::setAttribute
http://php.net/manual/ja/pdo.setattribute.php

上記の説明にある通り、プリペアードステートメントに対応していない、もしくは対応が完全ではないドライバの場合、PDO側でプリペアードステートメントをエミュレートするためのモードなんだそうです。

プリペアードステートメントは簡単にいえばSQLのテンプレートみたいものですが、セキュリティとしても、処理速度としても有効な仕組みなのでこれを使わない手はありません。

PDO側でどんなデータベースともプリペアードステートメントでやり取りできるようになっているなっている親切設計なのですが、このエミュレートモードの場合、int型がstring型で返ってしまう挙動があるようなのです。

型変換を回避する

結論から言うとPDO::ATTR_EMULATE_PREPARES属性をfalseにすることで型変換を回避することが可能です。

ドライバーの確認

これにはPDOでMySQLとのやり取りに使うドライバーがmysqlnd(MySQLNaitiveDriver)である必要があります。

おそらくPHP5.3以上であればmysqlndがデフォルトになっているかと思うので特別に何かする必要は無いとは思います。。

mysqlndはプリペアードステートメントに完全に対応しているドライバです(はず)。

ですのでこのドライバーをつかっている場合はPDOのエミュレートを使う必要はないのです。

もしかしたらPHP本体のビルドの際に有効にする形なのでパッケージやビルド経緯によっては有効になっていない場合もあるかもしれません。

有効になっているか確認したい場合はphpinfoで設定を出力してみてください。以下の様にビルドオプションで--enable-mysqlndが設定されている、mysqlndの項目があったら有効になっています。

phpinfo01
phpinfo02

mysqlndがビルド時に有効になっていない場合は有効にしてPHP本体をビルドし直すか、有効になっているPHPパッケージをインストールしてください。

型変換がされないようにエミュレートモードをOFFにしてみる

さて実際に試してみます。

以下のようなコードです。

以下のように出力されまシタ。

--queryメソッド--

array(3) {
  ["id"]=>
  int(1)
  ["name"]=>
  string(7) "ikariya"
  ["age"]=>
  int(72)
}
array(3) {
  ["id"]=>
  int(2)
  ["name"]=>
  string(5) "katou"
  ["age"]=>
  int(73)
}
array(3) {
  ["id"]=>
  int(3)
  ["name"]=>
  string(6) "takagi"
  ["age"]=>
  int(83)
}
array(3) {
  ["id"]=>
  int(4)
  ["name"]=>
  string(8) "nakamoto"
  ["age"]=>
  int(74)
}
array(3) {
  ["id"]=>
  int(5)
  ["name"]=>
  string(7) "shimura"
  ["age"]=>
  int(66)
}

--プリペアードステートメント--

array(3) {
  ["id"]=>
  int(1)
  ["name"]=>
  string(7) "ikariya"
  ["age"]=>
  int(72)
}
array(3) {
  ["id"]=>
  int(2)
  ["name"]=>
  string(5) "katou"
  ["age"]=>
  int(73)
}
array(3) {
  ["id"]=>
  int(3)
  ["name"]=>
  string(6) "takagi"
  ["age"]=>
  int(83)
}
array(3) {
  ["id"]=>
  int(4)
  ["name"]=>
  string(8) "nakamoto"
  ["age"]=>
  int(74)
}
array(3) {
  ["id"]=>
  int(5)
  ["name"]=>
  string(7) "shimura"
  ["age"]=>
  int(66)
}

ちゃんとint型はint型としてフェッチできていますね。これで型キャストを別途実施しなくてもリザルトの型のままで使える場面もでてきますね。

このようにエミュレーションモードを解除することで型変換を回避することが可能です。

補足・ハマったポイント

PDO::ATTR_STRINGIFY_FETCHESは?

属性にPDO::ATTR_STRINGIFY_FETCHESという「数字を文字列に変換する」というものがあり、これはturefalseを設定することでスイッチ的に使えるようで試してみましたが、なにも起こらず。

どうやらmysqlndは対応していない属性らしく(裏が取れていない)、この属性を設定することで変換を抑制することは(すくなくともMySQLでは)できないようです。

MySQL側での型変換

以下のようなコードでSQLを発行してみました。プリペアードステートメントです。

実行をMySQL側でログにとってみて実際どんなSQLとして発行されているか確認してみます。

2016-04-25T05:29:21.151379Z   21 Execute>---SELECT * FROM testtable WHERE id='5'

あれ?シングルコーテーションで囲まれていますね。文字列として扱われていますよ。 つまりこれMySQL側で型変換が行われているんですね…。

MySQL側の型変換の回避?

このことについては以下の記事が詳しく説明してくださっています。

A Day in Serenity (Reloaded) PDOでの数値列の扱いにはワナがいっぱい

ちなみにbind時に型を指定することで回避することが可能です。

$prepare->bindValue(":id", $id, PDO::PARAM_INT);

PDO::PARAM_INTint型を担保する設定で実際ちゃんとint型として扱われるます。

2016-04-25T05:31:55.904186Z   22 Execute>---SELECT * FROM testtable WHERE id=3

しかし、このPDO::PARAM_INTを設定したからといって必ずint型になるかといったら、そうはいかないようようです。

型変換の挙動の把握

このことについてはこちらの記事で詳しく説明してくださっています。

徳丸浩の日記 PDOのサンプルで数値をバインドする際にintにキャストしている理由

説明にあるように数値文字列をPDO::PARAM_INTでバインドしても、int型としては扱われないのですよ。

実際に@MINOも試しましたが…

$id      = '5';
$prepare->bindValue(":id", $id, PDO::PARAM_INT);

型変換されませんでした。文字列として扱われているのがわかるかと思います。

016-04-25T05:50:57.341878Z   23 Execute>---SELECT * FROM testtable WHERE id='5'

もしこれをint型で扱わせたいなら以下のようにキャストする必要があります。

$id      = '5';
$prepare->bindValue(":id", (int)$id, PDO::PARAM_INT);

int型で扱われています。

2016-04-25T05:55:05.598083Z   24 Execute>---SELECT * FROM testtable WHERE id=5

いろんなところで型変換をやってくれるPHPですが、こうして明示的に設定しても型変換をしてくれないのはなんだか落とし穴だなぁと思います。

いろいろな挙動を考慮する必要性

前述の記事にも言及されているとおり、intへのキャストはオーバフローの原因にもなる可能性もあり、キャストが解決策とは行かない場合もあるようです。

こうなってくるとPHP側で数値を厳密に扱う必要がありますね…。型宣言などでstring型を渡さないようにするとか…。

または先程の記事の最後で示されているSQLでの型の指定も良さそうです。

型を考慮したプログラミングを強く指向するなら、PHPを使う必要ある?という話になってきちゃう気もしなくはないですが…。

いずれにしてもPHPでデーターベースと連携する場合はPHPの型変換、PDOの挙動、データベースの型変換と挙動を考慮しないと、どこかで落とし穴がまっているということのようです。

@MINOはまだ大きなアプリケーションを作れる技量はありませんが、いつかココらへんの事情が関係したバグに見舞われるのでしょうか?

しっかし覚えておこう。

直接executeにパラメータを渡す場合

以下のようにexecuteの引数として値を与える場合、そのパラメータはstring型として扱われるようです。型を気にするなら、bindをした方が良いようです。

input_parameters
実行される SQL 文の中のバインドパラメータと同数の要素からなる、 値の配列。すべての値は PDO::PARAM_STR として扱われます。

PDOStatement::execute
http://php.net/manual/ja/pdostatement.execute.php

なので以下のようにintで値を与えていても、SQLでは型変換がおこなわれています。

//プリペアードステートメントとして
$prepare = $pdo->prepare($sql2);
$param = [":id"=>5];
$prepare->execute($param);
echo "\n--プリペアードステートメント--\n\n";
foreach ($prepare->fetchAll(PDO::FETCH_ASSOC) as $value) {
    var_dump($value);
}

string型として扱われています。

2016-04-25T06:36:19.230977Z   26 Execute>---SELECT * FROM testtable WHERE id='5'

まとめ

PHP7ではスカラ型の型宣言も搭載されているので、PHPであっても型の意識は重要になってきているイメージがあります。

特に各システムの境界面上での型の挙動は気にしていたほうが良いようです。

  • MySQLのリザルトがint型からstring型に変換されるのは仕様
  • 原因となっているのはPDOのプリペアードステートメントエミュレートモード
  • プリペアードステートメントエミュレートを解除することで変換を回避できる
  • PDO::ATTR_EMULATE_PREPARES=>falseでエミュレートモードがOFFに
  • MySQL側でも型変換が行われる場合がある
  • バインド時にPDO::PARAM_INTしてもキャストが行われる訳ではない
  • int型を保ちたいならキャストした上でPDO::PARAM_INTを設定する必要がある
  • ただし数字文字列の場合はオーバーフローの危険性もある
  • PHP、PDO、MySQLでの型変換や挙動を認識しておきたい