PHP
MySQL
PECL
handlersocket

PHPでHandlerSocketを使ってみる

More than 1 year has passed since last update.

HandlerSocketはMySQL用のNoSQLプラグインで、MariaDB5.3から標準で含まれるようになりました。

とある案件でPHPからHandlerSocket(以下HS)を使いたくなったのでそのメモ。


TL;DR


  • PHP用のHSクライアントとしてPECL拡張ライブラリを使う

  • ライブラリのドキュメントが古く、使い方が分からないのでソース読んだりして解読したよ


HSクライアント

Pure PHP版とかいくつかあるみたいですが、こちらのPECL拡張を使います。

https://github.com/kjdev/php-ext-handlersocketi

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を使い回したい場合とかで使うのかな?)

ちなみにこのindexidHandlerSocketi_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>をいれます。

複数指定したい場合は配列をいれることができます。

順序が保障されるのでプロトコル通り、openIndexindexで指定した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名, オペレータ, 値]のように指定します。

オペレータには=, >, >=, <, <=を使用することができます。