gallu’s blog

エンジニアでゲーマーで講師で占い師なおいちゃんのブログです。

大きなIDをどうやってPHPからMySQLに渡す?:詳細:PHPからcall編

とりあえず、ざっくりと接続してデータをfetchAllでゲトって、「ゲトれる事」をほんのりと確認してみましょう。
基本になるコードは以下の通り。
移行、SQL以降部分だけ適宜すりかえながらやっていきます。

<?php

// DB接続
require_once('config.php');
$dsn = 'mysql:dbname=test;host=localhost;charset=utf8mb4';

// 接続オプションの設定
$opt = array (
    PDO::ATTR_EMULATE_PREPARES => false,
);
// 「複文禁止」が可能なら付け足しておく
if (defined('PDO::MYSQL_ATTR_MULTI_STATEMENTS')) {
    $opt[PDO::MYSQL_ATTR_MULTI_STATEMENTS] = false;
}
// 接続
try {
    $dbh = new PDO($dsn, $user, $pass, $opt);
} catch (PDOException $e) {
    // XXX 本当はもう少し丁寧なエラーページを出力する
    echo 'システムでエラーが起きました';
    exit;
}
//var_dump($dbh);

// プリペアド
$sql = 'SELECT * FROM test;';
$pre = $dbh->prepare($sql);

// バインド
// XXX

// 実行
$r = $pre->execute(); // XXX

// データをまとめて取得
$data = $pre->fetchAll(PDO::FETCH_ASSOC);
var_dump($data);


結果

[gallu@hogehoge48 ~]$ php t.php
array(10) {
[0]=>
array(1) {
["i"]=>
int(9223372036854775806)
}
[1]=>
array(1) {
["i"]=>
int(9223372036854775807)
}
[2]=>
array(1) {
["i"]=>
string(19) "9223372036854775808"
}
[3]=>
array(1) {
["i"]=>
string(19) "9223372036854775809"
}
[4]=>
array(1) {
["i"]=>
string(19) "9223372036854775810"
}
[5]=>
array(1) {
["i"]=>
int(9223372036854775806)
}
[6]=>
array(1) {
["i"]=>
int(9223372036854775807)
}
[7]=>
array(1) {
["i"]=>
string(19) "9223372036854775808"
}
[8]=>
array(1) {
["i"]=>
string(19) "9223372036854775809"
}
[9]=>
array(1) {
["i"]=>
string(19) "9223372036854775810"
}
}

…………ほぉ。
戻り値がintだったりstringだったり。
面倒なんで省略しますが、test_autoテーブルも同じ挙動だったので、おそらく「INT_MAXを超える値の場合、値がぐらつかないように、stringで持ってくる」的なギミックがあるんだろうなぁ、と推測。


さて。
実際の動きとしては、大体
・テーブルからID(とそれ以外のデータの塊)を引っ張ってくる
Pageが存在して、それとは別に、大体IDを引数にして
・IDからテーブルを引っ張ってくる
的な動きをするケースってのは、捨て値で売りさばいても豪邸が立つくらいに大量に頻出する処理かと思われます。
「面倒だし興味あるから、一端、全体をなめたい」ので、foreachを使って、全体をなめてみます。
ちょいと全体が長いので、レコード件数の少ないtest_autoでテストしますが、testテーブルでも同じ結果でした。
まずは、一般的に推奨されていると思われる「暗黙の変換が起きないように、適切にintでキャストしてINT指定する」パターン。

// プリペアド
//$sql = 'SELECT * FROM test;';
$sql = 'SELECT * FROM test_auto;';
$pre = $dbh->prepare($sql);

// バインド
// XXX

// 実行
$r = $pre->execute(); // XXX

// データをまとめて取得
$data = $pre->fetchAll(PDO::FETCH_ASSOC);
//var_dump($data);

//$sql = 'SELECT * FROM test WHERE i = :i;';
$sql = 'SELECT * FROM test_auto WHERE i = :i;';
$pre = $dbh->prepare($sql);
foreach($data as $datum) {
var_dump($datum['i']);
    // バインド
    $pre->bindValue(':i', (int)$datum['i'], PDO::PARAM_INT);

    // 実行
    $r = $pre->execute(); // XXX

    // 取得して出力
    var_dump( $pre->fetchAll(PDO::FETCH_ASSOC) );
}


結果

[gallu@hogehoge48 ~]$ php t.php
int(9223372036854775806)
array(1) {
[0]=>
array(1) {
["i"]=>
int(9223372036854775806)
}
}
int(9223372036854775807)
array(1) {
[0]=>
array(1) {
["i"]=>
int(9223372036854775807)
}
}
string(19) "9223372036854775808"
array(1) {
[0]=>
array(1) {
["i"]=>
int(9223372036854775807)
}
}
string(19) "9223372036854775809"
array(1) {
[0]=>
array(1) {
["i"]=>
int(9223372036854775807)
}
}
string(19) "9223372036854775810"
array(1) {
[0]=>
array(1) {
["i"]=>
int(9223372036854775807)
}
}
string(19) "9223372036854775811"
array(1) {
[0]=>
array(1) {
["i"]=>
int(9223372036854775807)
}
}

ん……わかりやすく「切られてる」感じですねぇ。丸め誤差で丸められちゃってるんだか、INT_MAXで切られてるんだか。
いずれにしても、意図しているレコードではないレコード引っ張ってきちゃってるの感じ。
クエリログ覗いてみませう。

11064 Prepare SELECT * FROM test_auto WHERE i = ?
11064 Close stmt
11064 Execute SELECT * FROM test_auto WHERE i = 9223372036854775806
11064 Execute SELECT * FROM test_auto WHERE i = 9223372036854775807
11064 Execute SELECT * FROM test_auto WHERE i = 9223372036854775807
11064 Execute SELECT * FROM test_auto WHERE i = 9223372036854775807
11064 Execute SELECT * FROM test_auto WHERE i = 9223372036854775807
11064 Execute SELECT * FROM test_auto WHERE i = 9223372036854775807
11064 Close stmt

………あぁあ orz
切られてる切られてる。ドきっちり、切られてる(INT_MAXで切られてるのか丸め誤差で丸められてるのかは不明。アウトな時点で、それ以上の興味がわかなかったのでw)。
可能性としては
・指数表記で出力されてデータが取れない
・INT_MAXで切られる
丸め誤差に従って適切(適当)に丸められる
のあたりを予見していたので、まぁ予想通りっちゃぁ予想どおり。


お次。「文字列による暗黙の変換」を試してみる。
一応丁寧に「第二引数をstringでキャスト」+「第三引数に明示的にSTRを指定」。

// プリペアド
//$sql = 'SELECT * FROM test;';
$sql = 'SELECT * FROM test_auto;';
$pre = $dbh->prepare($sql);

// バインド
// XXX

// 実行
$r = $pre->execute(); // XXX

// データをまとめて取得
$data = $pre->fetchAll(PDO::FETCH_ASSOC);
//var_dump($data);

//$sql = 'SELECT * FROM test WHERE i = :i;';
$sql = 'SELECT * FROM test_auto WHERE i = :i;';
$pre = $dbh->prepare($sql);
foreach($data as $datum) {
var_dump($datum['i']);
    // バインド
    //$pre->bindValue(':i', (int)$datum['i'], PDO::PARAM_INT);
    $pre->bindValue(':i', (string)$datum['i'], PDO::PARAM_STR);

    // 実行
    $r = $pre->execute(); // XXX

    // 取得して出力
    var_dump( $pre->fetchAll(PDO::FETCH_ASSOC) );
}


結果

[gallu@hogehoge48 ~]$ php t.php
int(9223372036854775806)
array(1) {
[0]=>
array(1) {
["i"]=>
int(9223372036854775806)
}
}
int(9223372036854775807)
array(1) {
[0]=>
array(1) {
["i"]=>
int(9223372036854775807)
}
}
string(19) "9223372036854775808"
array(1) {
[0]=>
array(1) {
["i"]=>
string(19) "9223372036854775808"
}
}
string(19) "9223372036854775809"
array(1) {
[0]=>
array(1) {
["i"]=>
string(19) "9223372036854775809"
}
}
string(19) "9223372036854775810"
array(1) {
[0]=>
array(1) {
["i"]=>
string(19) "9223372036854775810"
}
}
string(19) "9223372036854775811"
array(1) {
[0]=>
array(1) {
["i"]=>
string(19) "9223372036854775811"
}
}

うんこっちだと取れる。


クエリログ確認

11069 Prepare SELECT * FROM test_auto WHERE i = ?
11069 Close stmt
11069 Execute SELECT * FROM test_auto WHERE i = '9223372036854775806'
11069 Execute SELECT * FROM test_auto WHERE i = '9223372036854775807'
11069 Execute SELECT * FROM test_auto WHERE i = '9223372036854775808'
11069 Execute SELECT * FROM test_auto WHERE i = '9223372036854775809'
11069 Execute SELECT * FROM test_auto WHERE i = '9223372036854775810'
11069 Execute SELECT * FROM test_auto WHERE i = '9223372036854775811'
11069 Close stmt

うん、こんな感じだよねぇ………暗黙の変換が気になる気になるw


一応、軽く無駄な抵抗を試みてみる。

$sql = 'SELECT * FROM test_auto WHERE i = cast(:i as UNSIGNED);';

ってやって、値を「明示的にキャスト」してみる。してみるだけ。


結果

[gallu@hogehoge48 ~]$ php t.php
int(9223372036854775806)
array(1) {
[0]=>
array(1) {
["i"]=>
int(9223372036854775806)
}
}
int(9223372036854775807)
array(1) {
[0]=>
array(1) {
["i"]=>
int(9223372036854775807)
}
}
string(19) "9223372036854775808"
array(1) {
[0]=>
array(1) {
["i"]=>
string(19) "9223372036854775808"
}
}
string(19) "9223372036854775809"
array(1) {
[0]=>
array(1) {
["i"]=>
string(19) "9223372036854775809"
}
}
string(19) "9223372036854775810"
array(1) {
[0]=>
array(1) {
["i"]=>
string(19) "9223372036854775810"
}
}
string(19) "9223372036854775811"
array(1) {
[0]=>
array(1) {
["i"]=>
string(19) "9223372036854775811"
}
}


クエリログ

11077 Prepare SELECT * FROM test_auto WHERE i = cast(? as UNSIGNED)
11077 Close stmt
11077 Execute SELECT * FROM test_auto WHERE i = cast('9223372036854775806' as UNSIGNED)
11077 Execute SELECT * FROM test_auto WHERE i = cast('9223372036854775807' as UNSIGNED)
11077 Execute SELECT * FROM test_auto WHERE i = cast('9223372036854775808' as UNSIGNED)
11077 Execute SELECT * FROM test_auto WHERE i = cast('9223372036854775809' as UNSIGNED)
11077 Execute SELECT * FROM test_auto WHERE i = cast('9223372036854775810' as UNSIGNED)
11077 Execute SELECT * FROM test_auto WHERE i = cast('9223372036854775811' as UNSIGNED)
11077 Close stmt

あぁよかったここにはプリペアド効くんだ。
効率の良し悪しは不明。ただまぁ「暗黙に変換される」よりは、まだしも「明示的な変換」のほうが、1mmほどはマシなんじゃなかろうかなぁ、っと。


とりあえずここまでで
・INT_MAXを超える値を、intでキャストするとちゃんと動かなくなる
状態が見えてきたので。
そうすると、STRING一択、(暗黙または明示による、SQL側での文字→数値)変換一択、しかとりあえず選択肢ないかなぁ、的な。


で、そうすると後は外部から入ってくるIDの、最低限のvalidate。
これについては、おいちゃんは ctype_digit() 関数がぴったり、だと思う。
注意点がないでもないんだけど。それについては「引数を明示的にstringでキャストすればいいじゃない」って思ってるw。そもそも「引数はstring」って明示されてるモノなんだし。
ちゃんとその辺を気にすると、PHP7に行った時に楽よ?w


その辺を踏まえての、DB接続以降の「ID受け取って1件取り出す」側の、サンプルっぽいブツ。

// データを取得したっぽい処理
$id = '9223372036854775810';
//$id = '92233720hoge'; // NGな値

// 超絶ざっくりvalidate
if (false === ctype_digit((string)$id)) {
    echo 'おかしなIDです';
    exit;
}

//$sql = 'SELECT * FROM test WHERE i = :i;';
$sql = 'SELECT * FROM test_auto WHERE i = cast(:i as UNSIGNED);';
$pre = $dbh->prepare($sql);

// バインド
$pre->bindValue(':i', (string)$id, PDO::PARAM_STR);

// 実行
$r = $pre->execute(); // XXX

// 取得して出力
var_dump( $pre->fetchAll(PDO::FETCH_ASSOC) );


とりあえず、こんな感じかなぁ、と。