Storh
PHP向けのファイルファーストなレコード保存。JSONCドキュメント、追記専用のセグメントログ、UUIDv7 IDを使うログベースのキュー。
composer require storh/storh最新リリース v0.5.2 MIT PHP 8.2+
$pages = $docs
->query()
->where('kind')->eq('page')
->orderBy('title')
->limit(20)
->get();実践例
2部構成のクイックスタート:ストレージルートにドキュメントを保存し、その隣にログエントリとキュージョブを追加します。
1つのルートに3つのエンジン
StorageRoot::resolve()はベースディレクトリと名前空間を正規化してルートを作り、各エンジンは初回使用時にその下に専用のディレクトリを作成します。
すべてのエンジンはStorageRecordオブジェクトを返し、id()とdata()を備えています。キューは各ジョブの取得と完了をイベントログに追記します。
use Storh\DocStore;
use Storh\Queue;
use Storh\RecordQuery;
use Storh\SegmentedLog;
use Storh\StorageRoot;
$root = StorageRoot::resolve(__DIR__ . '/var/storh', 'demo');
$docs = new DocStore($root, 'pages');
$home = $docs->put([
'kind' => 'page',
'title' => 'Home',
]);
$loaded = $docs->get($home->id());$log = new SegmentedLog($root, 'events');
$log->put([
'type' => 'page.saved',
'pageId' => $home->id(),
]);
foreach ($log->stream(RecordQuery::all()->after($home->id())->limit(50)) as $record) {
// Process records lazily.
}
$queue = new Queue($root, 'jobs');
$jobId = $queue->enqueue(['task' => 'render', 'pageId' => $home->id()]);
$job = $queue->claim();
if (null !== $job) {
$queue->complete($job->id());
}JSONCドキュメントストア
レコードごとに1つのJSONCファイルを使用し、UUIDv7ID、UUIDの末尾グループから取った2文字によるシャーディング、アトミックな置換、ストリーミング形式のフィールドフィルターに対応します。
セグメントログ
長さとチェックサムを前置したJSON行を追記専用のセグメントに保存。疎なインデックス、カーソルおよび時間範囲読み取り、クラッシュ復旧、圧縮に対応。
ログキュー
コンパクトな追記専用のキューイベントを使って、ジョブを保留中、処理中、完了の各状態に移します。
UUIDv7レコード
安定したカーソル、範囲指定スキャン、シャーディングに適した保存パスを実現する、並べ替え可能で時刻を特定できるID。
アトミックな書き込み
一時ファイルへの書き込みとアトミックな名前変更で、レコード全体を保持します。書き手がクラッシュした後は、次に開いたときに残された一時ファイルを掃除します。
書き込み途中の破損から復旧
セグメントを読み取る際に長さとチェックサムの各フィールドを検証し、復旧時に末尾の不完全な行を切り詰めます。
ファイルファースト
データベースサーバーもフレームワーク用アダプターも、Composerの実行時パッケージも不要です。呼び出し元はディレクトリを渡します。
JSONCを読み取り
レコードファイルにはコメントや末尾のカンマを含められます。storhはコンパクトな1行のJSONを書き込むため、コメントは次の書き込みまで残ります。
クエリビルダー
DocStoreとSegmentedLogではwhere('status')->in([...])のように流れる形でクエリできます。DocStoreは利用可能な場合、セカンダリインデックスを使います。
セカンダリインデックス
等価、ユニーク、範囲インデックスをコレクションと並ぶファイルとして保存。レコードから生成され、いつでも再構築できます。
スキーマ
任意のコレクションスキーマで各書き込みを検証し、インデックスを付けるフィールドを宣言できます。
SQLミラー
コレクションをSQLiteまたはMySQLに取り込み、ファイルを正本として保ったまま結合、検索、レポートに活用できます。使用時のみext-pdoまたはext-mysqliが必要です。
Storhを選ぶ理由
サーバーではなくディレクトリ
ベースディレクトリを渡すと、storhはその場所に書き込みます。必要なのはPHP 8.2とext-jsonだけで、Composerの実行時パッケージもフレームワーク用アダプターも不要です。
1つのレコード形式、3つの保存方法
ドキュメント、ログ、キューはすべて、UUIDv7 IDとデータの配列を保存します。用途に合わせてディスク上の形式を選び、3つすべてを1つのルートにまとめられます。
実行できる検査
verify()はインデックスとログの状態を、ファイルの全件スキャンと照合します。repair()は破損したレコードを隔離し、不完全なログ末尾を切り詰めます。CLIはcronジョブとヘルスチェック用にJSONを出力します。
条件を明示したベンチマーク
READMEには、PHP 8.5を搭載したApple M1 Proで、各エンジンにつき50,000件のレコードを処理した際の中央値が掲載されています。たとえばポイント読み取りは1回あたり5.6 µsです。すべての書き込みでfsyncを実行するため、書き込み速度はファイルシステムによって異なります。
との比較
storhはリレーショナルクエリエンジンではなくファイルファーストのストレージです。そのため、ここに挙げるツールはstorhが対象としていない処理に対応しています。
| ツール | 得意なこと | Storhとの違い |
|---|---|---|
| SQLite | 得意なこと単一ファイルで完結するSQLエンジン。プログラムのクラッシュや停電後も維持される、シリアライズ可能なACIDトランザクションを備えています。ドキュメントでは、場当たり的なデータファイルの代替として紹介されています。 | Storhとの違いstorhはレコードごとにJSONファイルを1つ使い、コアにPDOドライバーは不要です。複数レコードのトランザクションには対応せず、ドキュメントではその用途に実データベースを案内しています。ファイルを正本として保ったまま、コレクションをSQLiteに取り込んで結合やレポートに利用できます。 |
| MySQL | 得意なことマルチスレッドのSQLサーバーが提供するリレーショナルデータベースです。クライアントプログラムやライブラリ、管理ツールに加え、幅広いAPIを備えています。 | Storhとの違いstorhはPHPプロセス内でディレクトリを操作するため、サーバーを運用する必要はありません。結合、検索、レポートには、既存のmysqliハンドルを使ってコレクションをMySQLにミラーリングできます。ファイルが引き続き正本です。 |
| SleekDB | 得意なことPHPで動作するフラットファイル型のドキュメントデータベースです。サードパーティー依存がなく、スキーマ不要のデータをプレーンなJSONファイルに保存します。クエリAPIには結合、全文検索、group by、having、ネストされたプロパティのフィルターが含まれます。 | Storhとの違いstorhはドキュメントストアに追記専用のセグメントログとログベースのキューを組み合わせ、1つのルートにまとめます。各書き込みは保存完了を報告する前にfsyncされます。storh自体は結合に対応せず、結合にはコレクションをSQLiteまたはMySQLに取り込みます。 |
質問と回答
Storhのドキュメントから、簡潔に回答します。
はい。DocStoreはコレクション単位の書き込みロックで更新を直列化し、同じIDへの同時書き込みは最後の名前変更が優先されます。キューのジョブ取得中はキューロックが保持されるため、2つのワーカーが同じ保留中のジョブを取得することはありません。
はい。読み取りではコメントや末尾のカンマを受け入れますが、そのIDへの次のput()でファイルがコンパクトなJSON形式に書き直され、コメントと末尾のカンマは削除されます。TRUSTキャッシュモードでは、storhの外部で変更したファイルは検出されません。
ファイル数をファイルシステムが処理できる間は、IDによるポイント読み取りを高速に保てます。DocStoreのインデックスがないフィールドスキャンは約1万件を超えると遅くなるため、フィルター対象のフィールドにはインデックスを宣言してください。セグメントログなら、スキャンや範囲読み取りも約100万件まで無理なく扱えます。
書き込みはfsyncされてからアトミックな名前変更で公開されます。ログやキューを再度開くと、不完全な末尾は最後にコミットされたイベントまで切り詰められます。停電時にデータを保持できるかは、同期処理をファイルシステムとマウントオプションが守るかどうかによります。
いいえ。SqlMirrorを使ってコレクションをSQLiteまたはMySQLに取り込めば、結合、検索、レポートに利用できます。複数レコードへのアトミックな書き込みには、引き続きシステムの正本となる実データベースが必要です。
いいえ。Queueは汎用レコードストアではなく、ジョブのペイロードは完了後に破棄されます。
クイックスタートから始めて、「エンジンの選び方」を参考に、用途に合ったDocStore、SegmentedLog、Queueを選びましょう。