HandlerSocketはMySQL用のNoSQLプラグインで、MariaDB5.3から標準で含まれるようになりました。
とある案件でPHPからHandlerSocket(以下HS)を使いたくなったのでそのメモ。
TL;DR
- PHP用のHSクライアントとしてPECL拡張ライブラリを使う
- ライブラリのドキュメントが古く、使い方が分からないのでソース読んだりして解読したよ
HSクライアント
Pure PHP版とかいくつかあるみたいですが、こちらのPECL拡張を使います。
HS使いたいところってパフォーマンスが要求されるような場所なんで、少しでも速いやつがいいでしょうと。(比較したことないですが…)
ただ、リポジトリをみると分かるようにドキュメントは存在しません。
githubの前のリポジトリであるgoogle code上に多少ありますが、古くて使えないです。
↓にPHP classファイルの作りかけみたいのがあるので一応参考になるんですが、こちらも古いです。
https://github.com/kjdev/php-ext-handlersocketi/blob/master/handlersocketi%20class
(ソースをみるとfilterプロトコルに対応していそうですが、上記のclassにはオプションがありません)
PECL拡張のソースは読んだことなかったので、雰囲気(?)で読んでいきながらあとはローカルにインストールして試していきました。
(HSプロトコルは難しくないのでそんなには難しくない)
ちなみに今回の用途が参照だけだったので、参照系しかさわっていません><
constructor
PECL拡張をインストールするとHandlerSocketi
クラスが使えるようになります。
$hs = new \HandlerSocketi($host, $port, $options);
host名とport番号を指定してあとはoptionを渡せます。
host名以外はoptionalです。
option内は以下のようなパラメータを渡せるようです。
key | type | desc. |
---|---|---|
timeout | integer | connectionのtimeout、たぶんsec |
persistent | ? | 永続接続か都度接続かを指定できそうだが不明 |
すいません、力不足でpersistent
はよく分かりませんでした…
ソース上は以下の箇所ですが詳しい方がいらっしゃればコメントいただけるとありがたいです。
https://github.com/kjdev/php-ext-handlersocketi/blob/master/handlersocketi_class.c#L344-L348
openIndex
HSだとまず使用するindexを開く必要があります。
プロトコルのopen_index命令ですね。
上記のコードで作ったHandlerSocketi
インスタンスを使うと以下のように使えます。
/* @var \HandlerSocketi_index */
$hsi = $hs->openIndex($dbname, $table, $fields, $options);
インスタンスメソッドたちはsnake_caseのものとcamelCaseものがありますが、中身は同じでただのaliasになっているみたいなので、camelCaseを使います。
PHPなので。
$dbname
と$table
は説明不要だと思います。
$fields
は配列で取得したい列名を渡します。
プロトコルでいうとこの辺です。
肝心のINDEXを指定する引数がないですが、これは$options
内で指定します。
これはHSではINDEXが必須ではなく、省略するとPRIMARYが使われるためです。
$options
で指定できるパラメータは以下です。
key | type | desc. |
---|---|---|
index | string | openするindex名、省略されるとPRIMARYが使われる。なおPRIMARYを明示的に使う場合は大文字で渡す必要がある |
filter | string | array | filterで使用する列名、複数指定したい場合は配列を渡せます。プロトコルのこの辺 |
id | integer? | indexidを指定したいとき用なのかな? |
返り値はHandlerSocketi_index
インスタンスになります。
具体的なクエリはこのインスタンスに対して実行していくことになります。
id
についてはこの辺のindexid
を直接指定したい場合に使うんじゃないかと思われます。
基本は自分で指定しなくても、openIndex
内でいれてくれるので気にする必要はなさそうです。
(開いたindexを使い回したい場合とかで使うのかな?)
ちなみにこのindexid
はHandlerSocketi_index
インスタンスのgetId
メソッドから取得可能です。
find
参照時にメインで使うと思われるメソッドです。
プロトコルのfind命令ですね。
HandlerSocketi_index
インスタンスを使って以下のように使えます。
try {
$res = $hsi->find(['=' => [$user_id, $status]], ['limit' => 10, 'offset' => 5, 'safe' => true]);
} catch (\HandlerSocketi_Exception $e) {
var_dump($e);
}
引数の前に返り値の説明からします。
正常に実行されると取得データの配列が返されます。
注意すべき点としては連想配列では返ってこないということです。
純粋な2次元行列として返ってきますが、列の順序はopenIndex
で渡した$fields
の順序で保障されるので、array_combine
などを使えば連想配列にすることができます。
また失敗すると第二引数でsafe => 1
を指定すると失敗時に例外を投げるようになります。
エラーメッセージはgetError
で直近のエラーを取得可能ですが大した情報を返してはくれないようなので困りものです…
ものによってはなんのメッセージも入ってこないことがあるので役にたたないと思っておいた方がよさそうです。
第一引数は連想配列を渡します。
keyはプロトコロルでいうところの<op>をいれます。
=
, >
, >=
, <
, <=
をサポートしています。
valueはプロトコロでいうところの<vn>をいれます。
複数指定したい場合は配列をいれることができます。
順序が保障されるのでプロトコル通り、openIndex
のindex
で指定したINDEXのKEY列に対応します。
第二引数も連想配列を渡します。
パラメータは以下でいずれもoptionalです。
key | type | desc. |
---|---|---|
safe | boolean | trueだと失敗時にExceptionを投げる |
limit | integer | 下のoffsetとの組合せでSQLのLIMIT句と同様。省略すると1,0になる。limitだけを指定することも可能 |
offset | integer | 上記を参照 |
in | integer | string | array | WHERE IN (...) みたいなことをやりたいときに使う。使い方が若干分かりにくいところがあるのでで後述する |
filter | integer | string | array | 行取得の際のfilter。これも分かりにくいので後述 |
while | integer | string | array | 使い方はfilterとほぼ同じ、ただstop filterとして使う。こちらも後述する |
Schema
説明のために具体的なtableがあった方がいいので、以下のDDLを使用します。
CREATE TABLE `articles` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(255) DEFAULT NULL,
`body` text,
`status` enum('private', 'public', 'draft') DEFAULT 'draft',
`user_id` int(10) unsigned NOT NULL,
`created_at` int(10) unsigned NOT NULL,
`updated_at` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `i1` (`user_id`,`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
in
オプション
まずはPrimary Keyを使った以下のクエリをHSを使うとどうなるかをみてみます。
SELECT id, title, user_id, status FROM articles WHERE id IN (10, 20, 30) LIMIT 10;
LIMIT句はおまけです。
$hsi = $hs->openIndex($dbname, 'articles', ['id','title','user_id','status'], ['index'=>'PRIMARY']);
// $fake_idは無視される
$res = $hsi->find(['=' => $fake_id]], ['in' => [10, 20, 30], 'limit' => 10, 'safe' => true]);
/* [
[
10,
"title10",
...
],
[
20,
"title20",
...
],
[
30,
"title30",
...
]
]
*/
公式ドキュメントにあるように、find
の第一引数が無視されます。
次に複合INDEXを使う以下のクエリの場合をみてみます。
SELECT id, title, user_id, status FROM articles WHERE user_id = 100 AND status IN ('private', 'public') LIMIT 10;
INDEXi1
を使いたいわけですが、以下のようになります。
$user_id = 100;
$hsi = $hs->openIndex($dbname, 'articles', ['id','title','user_id','status'], ['index'=>'i1']);
// $fake_statusは無視される
$res = $hsi->find(['=' => [$user_id, $fake_status]], ['in' => [1 => ['private', 'public']], 'limit' => 10, 'safe' => true]);
/* [
[
10,
"title10",
100,
"public",
...
],
[
20,
"title20",
100,
"draft",
...
],
...
]
*/
複合INDEXの1列目であるuser_idをfind
の第一引数で指定しますが、複合INDEXなので配列を渡しています。
([$user_id, $fake_status]
のこと)
このうち今回は$fake_status
だけが無視されます。
これは第二引数のin
オプションで1
を指定しているためです。
つまりこの1
は第一引数のvalueで渡した配列の添字をさす、ということになります。
filter
オプション
filter
オプションを使用するために、以下のクエリを考えてみます。
(実用性は考慮しません)
SELECT id, title, user_id, created_at FROM articles WHERE
user_id = 100 AND created_at >= unix_timestamp()-86400 AND created_at < unix_timestamp()-3600
LIMIT 10;
HSだと以下のようになります。
$user_id = 100;
$now = time();
$hsi = $hs->openIndex($dbname, 'articles', ['id','title','user_id','status'], ['index'=>'i1', 'filter'=>['created_at']);
// $fake_statusは無視される
$res = $hsi->find(
['=' => $user_id],
[
'filter' => [['created_at', '>=', $now-86400], ['created_at', '<', $now-3600]],
'limit' => 10,
'safe' => true,
]
);
なかなか長くなってきましたね。
FILTERについてはここに書かれています。
filterとwhileの2種類があると書かれていますが、filter
オプションで使われるのはfilterの方です。
ややこしいですね。。
ちなみにwhileの構文は上記のfilter
とほぼ同じですが、挙動はドキュメントある通りにstop filterとして働くので、filterにmatchしないレコードが出現した時点で取得をやめます。
さてプロトコルの記述通り、filter(whileも)を使う場合はopen_index命令で使用するカラムを指定する必要があります。
上記のクエリだとcreated_at
をfilterで使うので、openIndex
メソッドの第二引数でfilter
パラメータとして渡します。
filterに複数条件を指定するとANDとして扱われるので、find
メソッドの第二引数のfilter
パラメータで複数条件を指定します。
中身は[field名, オペレータ, 値]
のように指定します。
オペレータには=
, >
, >=
, <
, <=
を使用することができます。