檜山さんがおもくそ先走って JSON ストレージの話を始めたので、ある意味で元凶たる俺も JSON ストレージの話をする。で、 JSON ストレージがどんなものかは檜山さんのエントリに書いてあるので、ここではどうやって JSON ストレージを実装するかの話ね(実は現在リリースしているバージョンにも JSON ストレージは同梱されているのだが、ぶっちゃけリリース2の作業の息抜きに半日ぐらいで作った代物なので、まあ参考にしたい人は参考にしてください程度の出来だ)。
基本的に JSON ストレージはバックエンド中立なライブラリで、例えば RDB をバックエンドに使ってもいいし普通のファイルに書き込んでしまってもいい。が、ひとまずは RDB を前提として話を進める。あとわかってると思うけど JSON スキーマの構文は標準として提案されてるものではなく Caty 構文を採用する。
JSON のようなデータを RDB に永続化する場合、大雑把に分けると構造写像とモデル写像の二つのアプローチがある。元々はどちらも XML を永続化するためのものだったと思うだが、入れ子構造の正規化されていないデータをそのまま永続化という意味では JSON も同じなので、ここから JSON を永続化する手段を模索する事にする。
二つのアプローチのうち簡単なのは構造写像で、これはプロパティ名=カラム名のテーブルにデータを永続化する手法だ。例えば以下のような JSON スキーマがあったとしたら
type person = object {
"name": string,
"birth": integer,
"job": string | null,
};
//データ例
{
"name": "鍬田力",
"birth": 1984,
"job": "プログラマ"
}
追記:クォーティングし忘れてたところを修正。
以下のようなテーブルを作成する。ここでの id はシステマティックに自動生成される人工キーとする。
| id| name|birth| job|
|0001|鍬田力| 25|プログラマ|
一見するとこのアプローチは上手く行きそうだが、次のような入れ子構造やリスト構造が入るとサブテーブルを作らなければならないのがネックだ。サブテーブルが入ると相関サブクエリと外部キーの嵐となり、わかりやすさが激減する。
type person = object {
"name": string,
"birth": integer,
"job": string | null,
"contacts": list [object {
"kind": string,
"address": string
}]
};
//データ例
{
"name": "鍬田力",
"birth": 1984,
"job": "プログラマ",
"contacts": [
{
"kind": "website",
"address": "http://return0.dyndns.org"
},
{
"kind": "twitter",
"address": "http://twitter.com/ckuwata"
}
]
}
もう一つのモデル写像は次のようなテーブル構造に永続化するアプローチで、こちらは入れ子やリストを上手く扱える。
| id| pid| ref| colname| value|
|0001|0001|null| null| null|
|0001|null|0001| name| 鍬田力|
|0001|null|0001| birth| 1984|
|0001|null|0001| job|プログラマ|
|0001|0002|0001|contacts| null|
|0001|0003|0002| 0| null|
|0001|null|0003| kind| website|
|0001|null|0003| address| (略) |
|0001|0004|0002| 1| null|
|0001|null|0004| kind| twitter|
|0001|null|0004| address| (略) |
ここでの id は論理的なレコードでユニークな値となる。また pid は null でなければ子要素を持っていることとなり、子要素は親要素を ref で参照する。実際のプロパティ名と値は colname と value に格納される。また、 colname が整数値のみのときはリストという規約にする。
このアプローチの欠点はテーブルを見ただけでは何が格納されてるのかさっぱりわからず、またクエリを発行すると鬼のような自己相関サブクエリの嵐になってこれまたわかりにくい(=デバッグが困難なものになる)。おそらく、事情を知らない第三者がみたら卒倒するだろう。ちなみに現在の仮実装は殆どこのアプローチをそのまま使っていて、その理由は複数テーブルにまたがる相関サブクエリとか発行するライブラリ書くなら自己相関サブクエリを打ちまくるライブラリを書く方がなんぼかマシだと思ったからだ。
そしてもう一つ、実装が終わってから思いついたというか気がついたアプローチがある。元々 SQL で木構造を扱うための手法に経路列挙モデルというものがあるが、これの応用だ。このアプローチなら、先のスキーマとデータを以下のように格納できる。
| id| path| value|
|0001| $.name| 鍬田力|
|0001| $.birth| 1984|
|0001| $.job|プログラマ|
|0001| $.contacts.0.kind| website|
|0001|$.contacts.0.address| (略) |
|0001| $.contacts.1.kind| twitter|
|0001|$.contacts.1.address| (略) |
Caty では JSON パス式というものを実装しており、例えば先のデータの「contacts の 0 番めのインデックスの kind の値」を "$.contacts.0.kind" のように表現するのだが、これはそのまま RDB に格納する時のパス名として使うこともできる。加えてオブジェクトを永続化する手続きだけでなく永続化された情報を元に戻す処理も素朴なモデル写像よりはだいぶ簡単なものになる。
というわけで、正式版の JSON ストレージはこのアプローチで作ることになると思う。まだ他のバックエンドに永続化するための手法はまだ考えていないが、まあそっちはおいおいやっていくということで。
この怪文書はクリエイティブ・コモンズ・ライセンスの元でライセンスされています。引用した文章など Kuwata Chikara に著作権のないものについては、それらの著作権保持者に帰属します。