はじめに
ブンブンHello Qiita、どうもみなさんこんにちは!
久しぶりの投稿になりますが、前回と同じく弊社Future Architectのアドベントカレンダー向け投稿です。
やれフロントエンドだ、やれJavaScriptだ、やれUrushi(弊社作OSSのWebコンポーネントFW)だの言っていた2年前とは打って変わって、現在はやれAWSだ、やれDockerだ、やれElasticsearchだといった感じで以前とは違ったことにチャレンジさせてもらってます。
という前置きは全く本編に関係ないのですが、まあとにかく最近DynamoDB、Elasticsearchでデータの親子関係をどう保持しようかという点で四苦八苦したのでその経緯から結論までまるっと記事にしたいと思います!
ちょっと簡単な前提
- 本記事では意味のあるデータの塊を「アイテム」と呼称します
- アイテムには親と子の関係が存在します
- 子アイテムはリビジョン管理します
- 親アイテムはリビジョン管理しません
- 親子両アイテムはDynamoDBに保存し、Elasticsearchへインデクシングします
- DynamoDBとElasticsearchへのCRUD処理を行うAPIを用意します
- 本記事では上記すべてをまるっと指して「サービス」と呼称します
「ふむ。いかにして表現すれば...」
検討に際し、まずはアイテムのデータ構造を整理しました。
エンタープライズ向け、ということでもちろん"業務項目"なるものが存在します。
それとは別に、今回"サービス"と呼んでいる領域において利用する管理用項目が存在します。(例えばシーケンスNoのようなものです。)
つまり本サービスの仕様・機能に関連する情報がサービス管理項目として保持され、その他サービスの挙動に影響を与えない項目群を業務項目と定義し、業務項目の中にはどんなデータが入っていてもサービス側は一切気にしないという構造です。
サービス管理項目と業務項目のどちらにどのような形で保持しようか?というところから検討を開始しました。
「そうだ!タグで表現しよう!」
今回の親子関係の話がでるより以前から、Elasticsearchを採用しているということでtagsの機能を利用して複数アイテムをグルーピングし検索できるとよいのではという話が出ていました。
そうだ!アイテムの親子関係もタグで表現してしまおう。
...闇の始まりでした。
「ねえねえ、それってタグで表現するメリットあるの?」
アイテムの親子関係をタグで表現しよう、とひとことで言っても色々な表現方法があります。(と当時の私は思っていました。)
もちろん考えなければいけないことはいろいろあります。
- 親子の情報は親子それぞれに保持させるのか
- 持ち方はサービスの利用用途に適しているか
- 持ち方はパフォーマンス面に懸念がない形になっているか
- 持ち方は実装難易度を不必要に上げる形となっていないか
「親子関係の紐付け」という操作を行うときに親と子のふたつのアイテムを更新したくない。
まずはそこから出発しました。
いま思えばなんでそう考えたのか明確には言葉にできませんが、親子のどちらかに情報をもたせるならまあそりゃ子だろうと私は考えました。
子に親が誰かという情報を持たせ、親には子の情報一切持たないという形です。
ParentItem {
// サービス管理項目
sequenceNo : 1,
category : movie,
Tags : [...],
// 業務項目
businessData : {...}
}
ChildItem {
// サービス管理項目
sequenceNo : 2,
revisionNo : 1,
category : photo,
Tags : [{"parent":"a"}...]
// 業務項目
businessData : {...}
}
ちょうどこんなかんじでイメージしました。
ただタグで表示するには厄介な問題がありました。
そう、アイテムをひとつ特定するためには複数のキー情報が必要だったのです。
上の例で言えば、「sequenceNo, category」がそれにあたります。
タグの中にその情報を入れ込もうとするとどうしても
{"parent" : ${sequenceNo}, "category" : ${category}}
といった感じになってしまいます。
「タグというのはね」...という枕詞でいつもわかりやすくタグについて説明してくれる先輩がいます。
その先輩の言葉を借りれば、「AWSを例にあげると、EC2なりDynamoDBなりにタグをつけるということは、例えばenvっていうキーに対してdevだとかstgだとかいうバリューをつけて、サービス利用者が任意の塊でグルーピングできるよということであって、その際AWS側はそのタグのキーが何でバリューにどんな値が入っているかは全く感知しない。」とのこと。
つまり「タグで表現します」と言いつつキーの値が固定になっている時点でそれはもはやタグではないと。
「なんでタグ使ってるのにそんな表現方法してるの?」
「それだったらいま固定キーで考えているところをそれぞれ項目として用意して保持すればよくない?」
まっとうな指摘でした。
「ねえねえ、それってタグで表現するメリットあるの?」
結局はこの質問にうまく回答できませんでした。
「ねえねえ、サービスの機能が業務項目に依存するってどういうこと??」
あー親子関係の表現方法決まらないな...と思いつつ、ここで私は少し迷走して的はずれな検討を進めてしまいました。
そうだ!親子の情報は業務項目に持たせればいいんだ!
私はそう考えました。
タグに持たせる必要はない = サービス管理項目で保持しない = 業務項目で保持する
...今思えばなんと愚かだったのだろうか。サービス項目を増やす、なんていうことは微塵も頭をよぎりませんでした。
ParentItem {
// サービス管理項目
sequenceNo : 1,
category : movie,
Tags : [...],
// 業務項目
businessData : {...}
}
ChildItem {
// サービス管理項目
sequenceNo : 2,
revisionNo : 1,
category : photo,
Tags : [...]
// 業務項目
businessData : {
"parent" : [{"no" : ${sequenceNo}, "category" : ${category},...],
}
}
よし、これだ!!
私は先輩にレビュー依頼を出し、ホワイトボードに検討結果を書きなぐりました。
...
...
...
「...ん?これ、サービスとして親子の管理するのをやめるって言ってる?」
「ねえねえ、サービスの機能が業務項目に依存するってどういうこと??」
つまり本サービスの仕様・機能に関連する情報がサービス管理項目として保持され、その他サービスの挙動に影響を与えない項目群を業務項目と定義し、業務項目の中にはどんなデータが入っていてもサービス側は一切気にしないという構造です。
あ...と思いました。
今回サービスが提供しているAPIの中には、アイテムの基本的なCRUD以外にも様々な機能がありました。
そのうちの一つが、今回の検討に大きく関わる「紐づく親あるいは子アイテムをすべて取得する」という機能です。
サービスの機能として紐づく親子を取得できるようにするということは、すなわちサービス管理項目として保持することを意味していました。
再検討。基本的な前提を抜かしてしまっていたので当然の結果でした。
「RelationTableというものがあってだな...」
我に返り設計を見直しました。
タグだとかなんだとか色々考慮していましたが、一旦初心に帰って検討を再開しました。
- そもそも今回親子の関係をサービスとして管理する必要はあるのか
- 結局親子関係はどう表現するのがよいか
- 性能面は本当に大丈夫なのか
まずは親子関係をサービスとして管理するかどうかについて。
これは、管理する、と結論づけました。
詳細は書けませんが、業務仕様としてアイテム群には親となりうるものと子となりうるものがあるということが明白でした。
業務的にアイテム間に親子関係が存在しているのであれば、それらアイテムを管理するサービスにて同様に親子関係を管理するほうがよいだろうと考えました。
また別の点からも管理はしてよいだろうと判断しました。
そもそも業務項目で保持しようと思ったきっかけは、ある間違った思考からです。
タグに持たせる必要はない = サービス管理項目で保持しない = 業務項目で保持する
元凶はここにありました。タグに持たせる必要がないこととサービス管理項目で保持しないことはイコールではありませんでした。
サービス管理項目を増やせばよいだけの話だったのです。
ChildItem {
// サービス管理項目
sequenceNo : 2,
revisionNo : 1,
category : photo,
Tags : [...],
parent : [{"no" : ${sequenceNo}, "category" : ${category},...],
// 業務項目
businessData : {...}
}
つまり、タグを利用しなくともサービス項目を増やせば親子関係の表現に不都合はなく、親子関係をサービスで管理することに対する阻害要因はゼロとなりました。
こうして親子関係の表現方法について検討を進めていたのですが、毎度つきまとう制約事項がありました。
トランザクション管理等の厄介な検討事項をなるべく減らしたいという思いから2フェーズコミットを避けたがゆえに、親子の情報は子の側にしか持たせないとしていました。
子アイテムから紐づく親アイテム群を取得したいときは、子に保持している情報から親を特定するキー情報が抽出できるので、そのまま親アイテムのリストを取得できました。
しかし、親アイテム起点でその親に紐づく子アイテム群を取得したい場合、親には子の情報を一切持っていないためフルスキャン必至でした。
ある日、先輩が一言。
「てかどうせフルスキャン走るなら子から親を検索するときのほうがよくない?レコード数的には子より親のほうが少ないわけだし。」
さらにもう一言。
「てか業務的に子から親を検索したいパターンってどんなとき?」
業務的な利用用途の視点を抜かしていました。
今回検討しているシステムでいうところの一覧画面とは、親に紐づく子の一覧を表示する画面がほとんどでした。
であればいままで子に持たせようとしていた情報を親に持たせれば解決するはず。
と思ったのですが、そもそも親→子あるいは子→親の検索時にどちらか一方はフルスキャン必至という前提もいかがなものかと。
...
...
先輩「RelationTableというものがあってだな...」
親アイテムあるいは子アイテムの中に親子関係を表す項目を保持しよう!
その方向性で頭を凝り固めてしまっていた私には衝撃の一言でした。
ついに決着。結論はいかに?!
そろそろこの長い戦いに決着をつけたい。私はその一心でした。
もちろん先輩は言わずもがなそう思っていたはずです。
検討は終盤を迎え、残ったカードは二枚。
- 親アイテムに子アイテムの情報をサービス管理項目として保持する
- RelationTableにて親子の関係性を保持する
軍配はRelationTable保持方式に上がりました。
決め手は、「2フェーズコミットを避けつつ、親→子&子→親のどちらの検索時でもパフォーマンスを期待できる」という点でした。
一旦この方針で進めようと話がまとまりました。
(現在絶賛検証中です。2017/12/10現在)
なにはともあれ方針が決定。本当に良かったです。
さいごに
ここまで読み進めてくださった方、ありがとうございます。
ツッコミどころはたくさんあるかとは思いますが、まずは私が最近四苦八苦した検討経緯の一部始終が伝えられたら幸いです。
「え?これ違くない?」
「いやいやここは前提がおかしいでしょ」
などなどあればコメントください!
最後にはなりますが、参考になったよーという方は"いいね"と"ストック"ぜひぜひよろしくお願いしますmm
冒頭にも書きましたが弊社Future Architectもアドベントカレンダーやってますのでよければぜひ!
ではでは(^^)
(YouTuber動画の見すぎで、はじまりとおしまいの挨拶が感化されてる感しかないですがそこはご容赦ください笑)