最近仕事で触っているサービスが階層構造を扱っていて、それが閉包テーブル (Closure Table) というパターンで実現されていますが、「結局(視覚的に)どういう構造になっているの?」というのがパッとみれたらいいなと思うことがあり、簡単なツールを作ってみることにしました。
できたものはこれです。
前提: 閉包テーブル (Closure Table) とは
閉包テーブルとは、「SQLアンチパターン」という書籍で紹介されているパターンです。階層的なデータ (例えばSNSのスレッド) をRDBで素朴にモデリングしようとすると、「あるスレッド内に存在するコメント一覧」をまとめてクエリしたくなったとしても、スレッドが無制限に続いたり途中で枝分かれしはじめるとクエリを書くのが大変になっていきます。
実際のデータが含まれているテーブルとは別に、階層関係を持つ「閉包テーブル」というテーブルをわけて持たせるようにすると簡単なクエリで目当てのデータが取得できるようになります。
本書では以下のようなテーブルを紹介しています。 TreePaths
テーブルに祖先と子孫のIDを持つカラムがあり、これによってあるコメントに紐づく子孫一覧などが簡単に取得できるようになります。
-- ※ 簡略化しています CREATE TABLE Comments ( comment_id, ... ); CREATE TABLE TreePaths ( ancestor, -- 祖先 descendant, -- 子孫 );
実際に TreePaths
テーブルに含まれるデータの例:
ancestor | descendant | 解説 |
---|---|---|
1 | 1 | 最も親となるコメント。自分への参照も持つので、ancestor=descendant |
1 | 2 | 1→2という関係 |
1 | 3 | 1→3という関係 (直属の子ではなく、孫の関係であっても閉包テーブルとしては持つ) |
1 | 4 | ↑と同様 |
2 | 2 | 2つめのコメント |
2 | 3 | 2→3という関係 |
2 | 4 | 2→4という関係 |
3 | 3 | 3つめのコメント |
4 | 4 | 4つめのコメント |
上記でポイントとなるのは、 1→2→3
や 1→2→4
という親子関係になっている場合において、2を飛ばして 1→3
や 1→4
の関係も持たせている点です。これによって、 ancestor=1
のデータをクエリするだけで、親もしくは先祖に1がいるコメントを全件取得したり、逆に descendant=4
でクエリすれば4を子もしくは子孫に持つコメントを全件取得できるようになっています。
行一覧を見ても、実際の構造がどうなっているのかよくわからない
先ほどの例で出した表をパッとみても、個人的には結局どういう階層構造になっているのかがいまいちわかりづらいと思いました。頭の中で図を描いていくと最終的にはわかりますが、1階層ずつたどる必要があって地味に難しいです。
途中で行が増減したり子が付け変わったりした場合に、閉包テーブルでは大きく行数が変動するので差分も若干わかりづらい気がします。ということで表題の話になりました(あと、単純にRustでCLIツールを何か作れたら面白そうというモチベーションもありました)。
木構造の可視化
あまり自分でグラフの描画を実装する気持ちになれなかったので、描画部分はPlantUMLやMermaidに任せることにしました。なので、閉包テーブルのデータをPlantUMLなどの別フォーマットに変換するだけのCLIツールを作ることにしました。
PlantUMLのサイトを見たところ、WBS図が一番木に近かったので、これに変換することにしました。
アスタリスクの数が多ければ階層が深い、みたいな雰囲気です。
@startwbs * A ** B *** D ** C *** E @endwbs
Mermaidについてはフローチャートを使うことにしました。
mermaid.js.org
PlantUMLと違って、Mermaidのフローチャートは階層を表していないので、自分でいい感じに表す必要があります。
flowchart TD A --> B B --> D A --> C C --> E
余談ですが、木構造専用の記法を追加してほしいというissueがMermaidのリポジトリに立っていることを調べていてたまたま発見しました。
作成したツールの使い方
JSON形式で閉包テーブルを渡すことを想定しています。また現状 ancestor
descendant
depth
の三つのkeyを持つオブジェクトの配列を想定して作ってあります。
以下のような input.json
というファイルが手元にあるとして、
[ { "ancestor": "A", "descendant": "A", "depth": 0 }, { "ancestor": "A", "descendant": "B", "depth": 1 }, { "ancestor": "A", "descendant": "C", "depth": 1 }, { "ancestor": "A", "descendant": "D", "depth": 2 }, { "ancestor": "A", "descendant": "E", "depth": 2 }, { "ancestor": "B", "descendant": "B", "depth": 0 }, { "ancestor": "B", "descendant": "D", "depth": 1 }, { "ancestor": "B", "descendant": "E", "depth": 1 }, { "ancestor": "C", "descendant": "C", "depth": 0 } ]
--format plantuml
でコマンドを実行すると、
closure2wbs --input input.json --output out.puml --format plantuml
以下のPlantUML形式のファイルが吐き出されます。
@startwbs * A ** B *** D *** E ** C @endwbs
同じように --format mermaid
でコマンドを実行すると、
closure2wbs --input input.json --output out.puml --format mermaid
以下のMermaid形式のファイルが吐き出されます。
flowchart TD A --> B B --> D B --> E A --> C
Rustについて
最近「Rustの練習帳」を少しだけ読んでいて、その中でRustでCLIツールを作るというのをやっていて、「もしかしたら簡単に作れそうかも」となったのでやってみました。
CLIツールに必要なオプションのパースとかは https://github.com/clap-rs/clap が勝手にやってくれて楽でした。一方でRustっぽい書き方できている自信はなくて、なんならGitHub Copilotがめちゃくちゃ活躍してくれたので、はたして..という感じです。