がるの健忘録 このページをアンテナに追加 RSSフィード

2017-03-22

[][]CAS実装……の前提のお話

ふと、色々と紆余曲折があった末として「MagicWeaponのdata_clumpで、cas形式の楽観的ロック、実装してみようかしらん?」というような発想がありまして。

その前提として「cas tokenど〜やって実装しよう?」から、比較的現実的な可能性があるあたりを妄想して、その辺の実装の欠片を思いついたので、おおむね、メモ用途でw


端的には「bigint(8byte) unsigned相当の整数」をランダムで持たせたら、大体、トークン足りえるんじゃないかなぁ? と、妄想をしまして。

8byteなら、index切っても、メモリ空間の圧迫が「そんなもんだろう」程度、だろうし。

渡し方については……MySQLであれば……二種類あるなぁ。


mysql> SELECT CAST(0xffffffffffffffff AS UNSIGNED);

+--------------------------------------+

| CAST(0xffffffffffffffff AS UNSIGNED) |

+--------------------------------------+

| 18446744073709551615 |

+--------------------------------------+

mysql> SELECT CONV('ffffffffffffffff', 16, 10);

+----------------------------------+

| CONV('ffffffffffffffff', 16, 10) |

+----------------------------------+

| 18446744073709551615 |

+----------------------------------+


………ん?

mysql> SELECT CAST('0xffffffffffffffff' AS UNSIGNED);

+----------------------------------------+

| CAST('0xffffffffffffffff' AS UNSIGNED) |

+----------------------------------------+

| 0 |

+----------------------------------------+

あぁ文字列にすると駄目なのか。

って事は、CONV関数だな。こっちなら「文字列」として渡せる。

まぁ、MySQLに方法があるんだ、ほかのRDBでもあるだろうw(ざつ)


んで。

だとするとあとは「8byte整数相当の値をランダムにとってくる」と、なんとなく行けるっぽくて。

後は「重複しねぇよなぁ」を、雑にテストしてみるとよいんじゃなかろうか、って思う。


Linux的に手っ取り早いのは /dev/urandom デバイスでしょ、ってんで、確認かねて、以下のコードを動かしてみた。

<?php

$data = [];
for($i = 0; $i < 100000; ++$i) {
    $h = bin2hex(file_get_contents('/dev/urandom', false, NULL, 0, 8));
    if (false === isset($data[$h])) {
        $data[$h] = 1;
    } else {
        echo "ng... \n";
    }
}

重複が発生したら教えてくれる、的な子。

とりあえず10万回。

……ま〜ま〜時間かかるのな。ほんのわずかにびっくりしたおw


何度か動かしてみましたが、(おおよそ予想通り)一度もぶつからなかったので。

多分、いけるんじゃないかなぁ、的な。


どこかで、気が向いたり体力が向いたりしたら、CASを実際に実装してみませう。

って事で、とりあえず「実装するために一番重要な部分」を、軽くテストしてみました。


突っ込みなどあったら歓迎いたしますので、是非。

2017-03-18

[]グローバルスコープ変数? filter_input関数? (それともNULL え・ん・ざ・ん・し?(はぁと))

ふいっと、filter_input関数を思い出しまして(って程度には使ってないw)。

機能的にはさほど大きな違いはないと思われるので、まずは性能をチェック。

基本は以下のコードで、コメントアウトを適宜はずしつつ測定。


<?php

//$_GET['hoge'] = 'a';

$t = microtime(true);
for($i = 0; $i < 100000; ++$i) {
    //$s = (string)@$_GET['hoge'];
    //$s = (string)filter_input(INPUT_GET, 'hoge');
    //$s = (string)($_GET['hoge'] ?? '');
    if (false === isset($_GET['hoge'])) {
        $s = '';
    } else {
        $s = (string)$_GET['hoge'];
    }
}
$t2 = microtime(true);
var_dump($t2 - $t);

結果一覧。


ある:(string)@$_GET['hoge'];

float(0.011772871017456)

float(0.017148971557617)

float(0.015825986862183)

float(0.014954805374146)

float(0.016114950180054)


ある:(string)filter_input(INPUT_GET, 'hoge');

float(0.032809972763062)

float(0.033557891845703)

float(0.02568507194519)

float(0.027527093887329)

float(0.02209997177124)


ある:null演算子

float(0.013915061950684)

float(0.016128063201904)

float(0.013824939727783)

float(0.014352083206177)

float(0.016026973724365)


ある:isset

float(0.023705959320068)

float(0.031208038330078)

float(0.013985872268677)

float(0.028561115264893)

float(0.022880077362061)


ない:(string)@$_GET['hoge'];

float(0.065753221511841)

float(0.078284025192261)

float(0.083561897277832)

float(0.048727035522461)

float(0.057971954345703)


ない:(string)filter_input(INPUT_GET, 'hoge');

float(0.014656066894531)

float(0.01726508140564)

float(0.016877889633179)

float(0.023527860641479)

float(0.022456169128418)


ない:isset

float(0.011105060577393)

float(0.0091698169708252)

float(0.0077798366546631)

float(0.0075478553771973)

float(0.0067591667175293)


ない:null演算子

float(0.010882139205933)

float(0.0078949928283691)

float(0.0078151226043701)

float(0.0083792209625244)

float(0.0070400238037109)


ふむぅ。

配列が「存在する」場合と「存在しない」場合で。

「ある」場合は、超雑な「(string)@$_GET['hoge'];」か、超丁寧な「isset」が双璧で早い感じ。

「ない」場合は、issetがさすがに「言語構造」なので最速、filter_inputがその後を追う感じ。

NULL演算子はいずれにも「隙がない」w。「PHP7以降」ってのが最大の欠点???


速度的にも「10万回ぶんまわして」一番遅いので80ミリ秒、なので。

どれでもそんなにかわらん気もするなぁ色々。


まぁ多分おいちゃん的には「基本的には"ある"前提で考える」ので、その辺の速度とか今までとかの兼ね合いを考えると

・雑に書きたいんなら相変わらず「(string)@$_GET['hoge'];」

・ただし、PHP7環境になったら光の速さでNULL演算子

って感じになるのが多そうかな?

2017-03-15

[]str_replace()の第一&第二引数は、文字列? 配列

結論から書くと、マニュアルに書いてある通り

search と replace が配列の場合、str_replace() は各配列から値をひとつ取り出し、 subject 上で検索と置換を行うために使用します。

なのですが……ちょっと驚いたので。


端的には、以下のコード。

<?php

$s = 'abc';

$r = str_replace(['a', 'b', 'c'], ['ca', 'cb', 'cc'], $s, $count);
var_dump($r, $count);

$r = str_replace(['c', 'a', 'b'], ['cc', 'ca', 'cb'], $s, $count);
var_dump($r, $count);

結果

string(8) "ccaccbcc"

int(5)

string(6) "cacbcc"

int(3)

配列の順番を変えると、結果が変わるんだねぇ………。

変換後の値がかぶらなければ順番は気にしなくてよいと思うのですが、変換後の値が「別の変換前の値」とかぶる場合、順番を気にする必要があるっぽです。

ただ「順番にやる」事が保証されるのであれば(マニュアルに書いてあるし)、それはそれで使い道が色々と別角度からは増えそうだなぁ、と。


で、少し疑問「だとすると、変換する配列の1組ごとに、全件一致検索毎回流しまくってるのかなぁ?」と。

ソースコード読むの面倒なんで、一端外側から叩いて、軽く確認。

<?php

$s = 'abc';

$t = microtime(true);
for($i = 0; $i < 100000; ++$i) {
    $r = str_replace(['c', 'a', 'b'], ['cc', 'ca', 'cb'], $s);
}
$t2 = microtime(true);
var_dump($t2 - $t);

$t = microtime(true);
for($i = 0; $i < 100000; ++$i) {
    $r = str_replace('c', 'cc', $s);
    $r = str_replace('a', 'ca', $r);
    $r = str_replace('b', 'cb', $r);
}
$t2 = microtime(true);
var_dump($t2 - $t);

結果

[gallu@hogehoge48 ~]$ php t.php

float(0.058500051498413)

float(0.056888103485107)

[gallu@hogehoge48 ~]$ php t.php

float(0.064057111740112)

float(0.06181788444519)

[gallu@hogehoge48 ~]$ php t.php

float(0.066102027893066)

float(0.065979957580566)

[gallu@hogehoge48 ~]$ php t.php

float(0.086111068725586)

float(0.082880973815918)

[gallu@hogehoge48 ~]$ php t.php

float(0.070822954177856)

float(0.057141065597534)

………ぶも??

予想としては「関数callのオーバヘッド分、少しだけ分割したほうが遅いけど誤差範囲」だと思ったのに……誤差範囲程度とはいえ「配列より文字列で複数回callしたほうが早い」???

あらら…てっきり「配列で渡したほうが効率よく文字列操作してくれる」と思ってたんですけどねぇ。


これは、今後少し、使い方を改めたほうがよいかしらん?

2017-03-14

[][]大きなIDをどうやってPHPからMySQLに渡す?:発端とまとめ

あるタイミングで、プリペアドステートメント回りのお話と、IDのカラム(によって決まる最大値)のお話と、intサイズのお話が別々に来た時に……頭ん中で混ざって、「あれ?」と思った事があったので調べてみた、って感じになります。


まず話に出ていたのが「AUTO_INCREMENTのカラムの型」のお話。

もうちょっと突っ込むと「intだと足りなくなる瞬間が以下略」。2147483647、ようは21億ちょい、なので、動かし方によっては十分に手が届いちゃう範囲。

int unsignedにしても4294967295だし、そもそもAUTO_INCREMENTなんで負の値いらないからまぁ「bigint unsignedだよねぇ」ってお話があって、これが素材その1。


bitintは8バイトなので、unsignedだと18446744073709551615とかいう、クソ怪しい値までOK……なんだけど。

PHPって、64bit環境の場合、あの子「signed intまでしか整数扱えない(以降は浮動小数点で扱う)」なので………あれ? 9223372036854775807 まではいいんだが、9223372036854775808以降は?

これが素材2。


PDOのプリペアドステートメントは「第二引数変数の型と第三引数の定数の両方がINT」以外はstring扱いだよねぇ、って話が、全然別の流れで普通に出てきて。

………あれ? 「PHP的にintで扱えない値はどうなるの?」って疑問がむくむくと湧いてきて。

これが素材3。


というわけで、素材1〜3までを合成………するまえに。

とりあえず、最低限のチェックから。

<?php

// PHP_INT_MAXの「次の数」の確認
$i = PHP_INT_MAX;
$i += 1;
echo "---\n";
var_dump($i);
printf("%f\n", $i);
echo "---\n";
var_dump((int)$i);
var_dump(intval($i));
echo "---\n";
printf("%d\n", (int)$i);
printf("%d\n", intval($i));
echo "---\n";


// 少し雑に「大きな数」の確認(INT_MAXの先頭に1追加した数値)
$i = 19223372036854775807;
echo "---\n";
var_dump($i);
printf("%f\n", $i);
echo "---\n";
var_dump((int)$i);
var_dump(intval($i));
echo "---\n";
printf("%d\n", (int)$i);
printf("%d\n", intval($i));
echo "---\n";

---

float(9.2233720368548E+18)

9223372036854775808.000000

---

int(-9223372036854775808)

int(-9223372036854775808)

---

-9223372036854775808

-9223372036854775808

---

---

float(1.9223372036855E+19)

19223372036854775808.000000

---

int(776627963145224192)

int(776627963145224192)

---

776627963145224192

776627963145224192

---

うわぁい(苦笑

intでキャストもintval関数も、「負の値に行ったり(これはまぁわかる)」、よくわからん数値になってきたり(多分単純にビットあふれが切り捨てられてる)。

まぁ、マニュアルにも書いてあるしなぁ


http://php.net/manual/ja/function.intval.php

最大値はシステムに依存します。32 ビットシステムでは、 最大の符号付き整数の範囲 -2147483648 〜 2147483647 となります。 このため、そのようなシステムでは intval('1000000000000') は 2147483647 を返します。 64 ビットシステムにおける最大の符号付き整数は 9223372036854775807 となります。


なので、大きなIDを

・受け取って

・INTでキャストして

プレースホルダにバインド

すると、多分間違いなくなんとなくまずもっておそらく「NGであろうなぁ」と思われるに至り、これは「実験せにゃなぁ」と思ったわけでございます。


で…細かい実験は長いんで、後で書きますが。

ものすごく端的に要約すると、現時点のおいちゃん見解としては、大体以下の通りかなぁ、と。

・(IDなんで演算とかしない前提で)文字列で受け取り、文字列で渡すようにする

・WHERE句のIDに「明示的にCASTするかどうか」は、お好みで。可能性として「CASTしておいたほうが効率が良い、かも、しれない」のと「何となくせめて明示したい」w

・IDはvalidateする。ctype_digit()関数がよいと思う

・「文字列から数値への、WHERE句での使用時の暗黙の(またはCASTによる)変換」の挙動が変わらないように、祈るw


いやまぁ「例えば、bigint unsignedを避ける(bigint signedにする)」+「PHPは64bit環境」って選択肢もないわけではないのですがw

まぁ少ないとは思うのですが、サービスの余命考えた時に「どっちかねぇ?」って感じになると思うのです。


いやまぁ実際、現時点のおいちゃん見解も「ど〜かねぇ?」とは思うのですが。暗黙の変換の、しかも「明記されているわけではない」挙動に頼る、ってのも(CAST使えば"暗黙"ではないですが一応)。

ただ、現状、それ以外に今一つ「よいアイデア」が浮かばないんですよねぇ……というわけで、現状における「苦肉の策」だと思っていただけると。と。

なので。「普通の整数を扱う」ときはちゃんとINTで扱って、ただ「INT_MAXを超える可能性があり」かつ「算術演算が発生しない」AUTO_INCREMENTなIDについて」のみ、例外的に、上述のような方法を取らざるを得ないのかなぁ、という。


もうちょっと妙手があればなぁ、と思うので、コメントなどありましたら、お気軽によろしくお願いいたします!!

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

とりあえず「思考の流れと調査の流れ」をそのまま書くので、読みにくいのはごめんなさいまし。


まずはいくつか検証テーブルを用意…する前に、MySQL自体の動きの確認を。

直近、参考にさせていただいたのは、hnwさんのこちらのサイト。

http://d.hatena.ne.jp/hnw/20120405


例題にある、以下のSQLを実際に流して、動きの確認をしました。

確認したのは、Server version: 5.6.10。ちょいと古いんだけど、ちょうど年末の整理でつぶす直前のサーバだったので、気兼ねなくw

create table decimal_test(id integer auto_increment primary key, a decimal(20));

insert into decimal_test(a) values(9007199254740992),(9007199254740993),(9007199254740994),(9007199254740995),(9007199254740996);

select * from decimal_test where a='9007199254740993';

mysql> select * from decimal_test where a='9007199254740993';

+----+------------------+

| id | a |

+----+------------------+

| 1 | 9007199254740992 |

| 2 | 9007199254740993 |

+----+------------------+

2 rows in set (0.00 sec)

うん、記述通り。


create table bigint_test(id integer auto_increment primary key, a bigint);

insert into bigint_test(a) values(9007199254740992),(9007199254740993),(9007199254740994),(9007199254740995),(9007199254740996);

select * from bigint_test where a='9007199254740993';

mysql> select * from bigint_test where a='9007199254740993';

+----+------------------+

| id | a |

+----+------------------+

| 2 | 9007199254740993 |

+----+------------------+

1 row in set (0.00 sec)

こちらも記述通り。


とりあえず、考察の前提が「AUTO_INCREMENTでの値」のお話なんで

・計算はしない

・型はint系(っていうかぶっちゃけるとbigint unsigned一択)

で、以降お話をすすめます。


さて。

MySQLマニュアルにも、これに近しいお話は書いてありまして。

文字列カラムと数字との比較では、MySQL はカラム上のインデックスを使用して、値をすばやく検索できません。str_col がインデックスの付いた文字列カラムである場合は、次のステートメントで検索を実行するときに、そのインデックスを使用できません。

-略-

その理由は、'1'、' 1'、'1a' のように、値 1 に変換できるさまざまな文字列があるためです。

このような数字は不正確であるため、浮動小数点数 (または浮動小数点数に変換される値) を使用する比較は概算になります。これにより、整合性のない結果が表示される可能性があります。

ってなわけで、書いてある内容を試してみます。


書いてあるのは

mysql> SELECT '18015376320243458' = 18015376320243458;

-> 1

mysql> SELECT '18015376320243459' = 18015376320243459;

-> 0

実験。

mysql> SELECT '18015376320243458' = 18015376320243458;

+-----------------------------------------+

| '18015376320243458' = 18015376320243458 |

+-----------------------------------------+

| 1 |

+-----------------------------------------+

1 row in set (0.01 sec)

mysql> SELECT '18015376320243459' = 18015376320243459;

+-----------------------------------------+

| '18015376320243459' = 18015376320243459 |

+-----------------------------------------+

| 1 |

+-----------------------------------------+

1 row in set (0.00 sec)

………まてこら。話が違う!!

ただ、実はちょいと別の角度から、怪しい動きをします。

数値をちょいと変えるよ & 大体「カラム=値」で聞くのと、カラムのほうが数値型なので、順番も入れ替え。


mysql> SELECT 9223372036854775808 = '9223372036854775808';

+---------------------------------------------+

| 9223372036854775808 = '9223372036854775808' |

+---------------------------------------------+

| 1 |

+---------------------------------------------+

1 row in set (0.00 sec)

mysql> SELECT 9223372036854775808 = '9223372036854775809';

+---------------------------------------------+

| 9223372036854775808 = '9223372036854775809' |

+---------------------------------------------+

| 1 |

+---------------------------------------------+

1 row in set (0.00 sec)

mysql> SELECT 9223372036854775808 = '9223372036854775810';

+---------------------------------------------+

| 9223372036854775808 = '9223372036854775810' |

+---------------------------------------------+

| 1 |

+---------------------------------------------+

1 row in set (0.00 sec)


ほら(悪い方に)あたった。丸め誤差出てる。

ほかのすべてのケースでは、引数浮動小数点 (実) 数として比較されます。

って記述があるので、大体状況の予想はできる。


…んと

mysql> SELECT '9223372036854775808' + 0;

+---------------------------+

| '9223372036854775808' + 0 |

+---------------------------+

| 9.223372036854776e18 |

+---------------------------+

1 row in set (0.08 sec)

mysql> SELECT '9223372036854775809' + 0;

+---------------------------+

| '9223372036854775809' + 0 |

+---------------------------+

| 9.223372036854776e18 |

+---------------------------+

1 row in set (0.00 sec)

mysql> SELECT '9223372036854775810' + 0;

+---------------------------+

| '9223372036854775810' + 0 |

+---------------------------+

| 9.223372036854776e18 |

+---------------------------+

1 row in set (0.00 sec)

こーゆー感じ。

倍精度ったって限界はあるので。このあたりの数値で「1違う」値って、誤差で吸収されて「同じ値」になっちゃったりするのだよねぇ。

………じゃぁ「select * from bigint_test where a='9007199254740993';」がうまくいった、丸め誤差に巻き込まれなかった理由はなにか?


確定できるわけではないのですが、こんな風に組み替えると、こんな風に結果が変わります。

mysql> SELECT 9223372036854775808 = CAST('9223372036854775808' as UNSIGNED);

+---------------------------------------------------------------+

| 9223372036854775808 = CAST('9223372036854775808' as UNSIGNED) |

+---------------------------------------------------------------+

| 1 |

+---------------------------------------------------------------+

1 row in set (0.02 sec)

mysql> SELECT 9223372036854775808 = CAST('9223372036854775809' as UNSIGNED);

+---------------------------------------------------------------+

| 9223372036854775808 = CAST('9223372036854775809' as UNSIGNED) |

+---------------------------------------------------------------+

| 0 |

+---------------------------------------------------------------+

1 row in set (0.00 sec)

mysql> SELECT 9223372036854775808 = CAST('9223372036854775810' as UNSIGNED);

+---------------------------------------------------------------+

| 9223372036854775808 = CAST('9223372036854775810' as UNSIGNED) |

+---------------------------------------------------------------+

| 0 |

+---------------------------------------------------------------+

1 row in set (0.00 sec)


なんとなし「実数(浮動小数点)変換」のケースと「整数変換」のケースがあるんじゃないかなぁ、という予想が、少しばっかり*1

上述を踏まえて、少し「実際にテーブル作って」挙動の確認をしてみたいかなぁ、と思います。


下準備。INSERT、まぁしないのですが「文字と数値」で、両方の値を入れてみましょう(少し蛇足な実験w)。

あとは、本来的にターゲットであるAUTO_INCREMENT。

DROP TABLE IF EXISTS test;

CREATE TABLE test (

i bigint unsigned

)ENGINE=InnoDB;

INSERT INTO test VALUES (9223372036854775806),(9223372036854775807),(9223372036854775808),(9223372036854775809),(9223372036854775810);

INSERT INTO test VALUES ('9223372036854775806'),('9223372036854775807'),('9223372036854775808'),('9223372036854775809'),('9223372036854775810');


DROP TABLE IF EXISTS test_auto;

CREATE TABLE test_auto (

i bigint unsigned NOT NULL AUTO_INCREMENT,

PRIMARY KEY (i)

)ENGINE=InnoDB auto_increment = 9223372036854775806;

INSERT INTO test_auto VALUES (NULL), (NULL), (NULL), (NULL), (NULL), (NULL);


下確認

mysql> SELECT * FROM test;

+---------------------+

| i |

+---------------------+

| 9223372036854775806 |

| 9223372036854775807 |

| 9223372036854775808 |

| 9223372036854775809 |

| 9223372036854775810 |

| 9223372036854775806 |

| 9223372036854775807 |

| 9223372036854775808 |

| 9223372036854775809 |

| 9223372036854775810 |

+---------------------+

10 rows in set (0.00 sec)

すんなりと入ってます。


さて、比較演算子を実験。

mysql> SELECT * FROM test WHERE i = 9223372036854775809;

+---------------------+

| i |

+---------------------+

| 9223372036854775809 |

| 9223372036854775809 |

+---------------------+

2 rows in set (0.00 sec)


mysql> SELECT * FROM test WHERE i = '9223372036854775809';

+---------------------+

| i |

+---------------------+

| 9223372036854775809 |

| 9223372036854775809 |

+---------------------+

2 rows in set (0.00 sec)

mysql> SELECT * FROM test_auto WHERE i = 9223372036854775809;

+---------------------+

| i |

+---------------------+

| 9223372036854775809 |

+---------------------+

1 row in set (0.00 sec)


mysql> SELECT * FROM test_auto WHERE i = '9223372036854775809';

+---------------------+

| i |

+---------------------+

| 9223372036854775809 |

+---------------------+

1 row in set (0.00 sec)

ふむぅ。

単純に「SELECTに比較演算子を与えて出力」ん時は丸め誤差巻き込んでtrue返してたのに、実際のWHERE句になると挙動が違う。

どっちかってぇと「CASTした時」に近いような印象を受ける感じですねぇ。


まぁ、ここについてはhnwさんのほうでも

マッチしたのは1レコードだけでした。このことから、BIGINT型の値と文字列リテラルとを比較した場合は64ビット整数として比較されていることがわかります。

-壮絶に中略-

MySQLの数値型と文字列型の比較は浮動小数点数比較になるようにマニュアルに書いてありますが、整数比較になる例外ケースがいくつか見つかりました。具体的には、BIGINT型と文字列リテラルインデックスつきのDECIMAL型と文字列リテラル、という2組が整数比較されているとわかりました。

って書かれてますし。

細かい話をすると「WHERE句だけなんかなぁ?」とも思うのですが、まぁ、使うし使いたいのはWHERE句で、なんで、気にしないw


また、MySQLマニュアルのほうにも

https://dev.mysql.com/doc/refman/5.6/ja/numeric-type-overview.html

BIGINT カラムについて注意の必要な点は、次のとおりです。

-中略-

文字列を使用して格納すると、いつでも正確な整数値を BIGINT カラムに格納できます。この場合、MySQL は、中間倍精度表現を含まない文字列から数値に変換します。

とあるので。

上述は「格納」ですが、比較についてもなんとなし「中間倍精度表現を含まない文字列から数値に変換」ってロジックが流れている可能性が想起されるような気が、びしばしとするのでございます。


このあたりを踏まえたうえで、じゃぁ次は「PHPからcallするとどうなるの?」っていう、疑問のメインをあさってみたいと思います。

[][]大きな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) );

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

*1:余談ですが。CASTで「実数への変換」って、ないんですかねぇ? いや「ちゃんとマニュアル読み切れてないだけ」な気もするのですが

2017-03-13

[][]異字体

大本ネタはこちら。

http://moriyoshi.hatenablog.com/entry/2017/03/13/011005

ここで「異体字セレクタ」ってのをはじめて知って、ちょろっと調べものをしたので、備忘録的に。

UnicodeっつかUTF-8で「1文字づつ」に分割するごにょごにょを書きたいかもなぁ、とか、ちょろっと妄想をしていたので。


とりあえず。

(二点しんにょう U+8FBB + VS18) ⇔「辻??」(一点しんにょう U+8FBB + VS17)

……ふお?

U+8FBB はわかるんだが、 + VS18、って、なにかしらん?

が、startライン。


http://itpro.nikkeibp.co.jp/article/COLUMN/20110124/356398/?rt=nocnt

見ると、「E9 82 8A F3 A0 84 80」とか「E9 82 8A F3 A0 84 81」とか。


念の為に、UTF-8エンコーディング方法を確認。

https://ja.wikipedia.org/wiki/UTF-8

………ぶも?

5バイト長とか6バイト長とかある???

一端より道して調べもの。…いやもしあるんなら。MySQLのutf8mb4が、些か、怖いやもしれぬので。


https://ja.wikipedia.org/wiki/Unicode%E4%B8%80%E8%A6%A7%E8%A1%A8

http://www.unicode.org/roadmaps/


ふむ。第16面でも「U+F0000 〜 U+10FFFF」なのねん。

これだと十分に4バイトで入る。…5バイト長とか6バイト長は「将来に向けての布石」なのかしらん?

とりあえず「当面の厄介」は消えたぽいので、「5バイト長とか6バイト長」の調査は一端ここで打ち切り


もどして…とりあえず、ビット列に分解して、有効な値を取り出してみる。


E9 82 8A F3 A0 84 80

1110 1001 1000 0010 1000 1010 1111 0011 1010 0000 1000 0100 1000 0000


E9 82 8A F3 A0 84 81

1110 1001 1000 0010 1000 1010 1111 0011 1010 0000 1000 0100 1000 0001


使う子だけ、はじき出す…とりあえず先頭は多分3バイトのはず。

1110 yyyy 10yx xxxx 10xx xxxx

1110 1001 1000 0010 1000 1010 1111 0011 1010 0000 1000 0100 1000 0000

1110 1001 1000 0010 1000 1010 1111 0011 1010 0000 1000 0100 1000 0001


どっちも

1001000010001010

908A


うん確かに「邊」の字だ。

てことは手前3バイトは文字なんだ…後ろの4バイトを解析してみませう。

先頭が11110なんで、UTF-8的には「4バイト文字」のはず。


1111 0yyy 10yy xxxx 10xx xxxx 10xx xxxx

1111 0011 1010 0000 1000 0100 1000 0000

1111 0011 1010 0000 1000 0100 1000 0001


011100000000100000000

011100000000100000001


E0100

E0101


一覧を見てみる…

https://ja.wikipedia.org/wiki/Unicode%E4%B8%80%E8%A6%A7_E0000-E0FFF

「VS17」とか「VS18」とか。


あぁ!!

単純に、ここらへん(おそらく、E0100〜E01E0F)までは「異字体(IVS:Ideographic Variation Sequence)だよ!!」って取決めなのか!!

うん、腑に落ちた。


ってことは「1文字を切り出す」時に、もし後ろにこの子がいたら「前の字とセットで」持っておいてあげる必要があるんだなぁ。

PHPの実装とかどうなってるんだろ?

まぁその辺はまた後日、気になったら調べてみませうw