やっぱり MapReduce むずいけど ...
またまた自分メモ。
CouchDBのJOINはこのあたりが詳しいが単純なケースにしか通用しない、という話。以下、今書いているブログアプリを例に取る。
ユーザー毎にブログを作成できるようにしたいので、Setting => Entry => Comment のような関連ができる。Setting はブログの設定、アクセス制御とかその辺のパラメーターをもっていて、Entryはエントリーそのもの、Comment はエントリーにつけられたコメント。
で、まぁとりあえず、Setting と Entry はドキュメントを別々にする。1ブログ1Setting, N Entryドキュメント。で、Comment は難しいところだが、Entryドキュメントないに comments という配列でつっこむことにした。正規化なんてしない!コメントはどうせ追加するだけで、更新も削除することもまれなので。
ここで問題が。すべての公開されているブログの最近のエントリー一覧というビューがうまく定義できない。
// Setting { id: "yssk22", permission_view : {all :true, tags: [...] }, updated_at : ... } // Entry { id: "{UUID}", setting_id : "yssk22", title : "...", content : "...", comments : [], updated_at: ... }
こんな感じで、setting.permission_view.all == true が公開ブログという意味。公開ブログを取り出すだけであれば、
function(doc){ if( doc.type == "Setting") { if(doc.setting.permission_view.all){ emit(doc.id, doc); } } } || だが、公開ブログのエントリーを取り出すとなると、Setting と Entry でJOINが必要。 >|js| function(doc){ if( doc.type == "Setting") { if(doc.setting.permission_view.all){ emit([doc.id, 0], doc); } } else if( doc.type == "Entry") { emit([doc.setting_id, 1], doc); } } || ここまではOKで、「最近のエントリー」 という話になると、updated_at をkeyに絡ませる必要がある。 >|js| function(doc){ if( doc.type == "Setting") { if(doc.setting.permission_view.all){ emit([doc.id, 0], doc); } } else if( doc.type == "Entry") { emit([doc.setting_id, 1, doc.updated_at], doc); } } || これで、「あるブログに関して設定と最近のエントリーN件」を取り出すことは可能。問題は、順番が逆で、すべてのブログエントリについてupdated_atの降順にならべて、その中から公開ブログのエントリーを取り出す、となるので、 >|js| function(doc){ if( doc.type == "Setting") { if(doc.setting.permission_view.all){ emit([null, doc.id, 0], doc); } } else if( doc.type == "Entry") { emit([doc.updated_at, doc.setting_id, 1], doc); } } || な感じで、updated_at をキーの先頭に持ってくる必要がある。しかし、これだと、null, doc.updated_at で集められてしまうので、JOINが成立しない。 じゃ、いっそのこと、 >|js| function(doc){ if( doc.type == "Entry") { emit(doc.updated_at, doc.setting_id); } } || で、最新の更新順にsetting_id を必要な件数(m件)取り出して、 >|js| function(doc){ if( doc.type == "Setting") { emit([setting.permission_view.all, doc._id], null); } } || これで、startkey=[true]&endkey=[true, "\u0000"] で取り出せた、エントリーだけ残すようにする、という案。しかし、例えば、全部でN件のエントリーがあったとして、すべてのエントリーのブログの設定が permnission_view.all==false だとした場合に、N / m * 2 回のビューリクエストが発生するので、m=50, N = 10000 だとしても200回であって、おーい、という話だ。Web見知りをする30人が毎日1エントリーを起こすと、1年でこの規模に達する。 というわけで解決案。 Setting 側に最新のエントリー数件はidのリンクを持たせる。 >|js| // Setting { id: "yssk22", permission_view : {all :true, tags: [...] }, updated_at : ..., recent_updated: [{entry_id : "id1", updated_at: ...}, ...] }
これであれば、
function(doc){ if( doc.type == "Setting") { if(doc.setting.permission_view.all){ for(var i in doc.recent_updated_ids){ emit(doc.recent_updated_ids[i].updated_at, doc.recent_updated_ids[i].entry_id); } } } }
で、更新日順に並び替えられたエントリーIDの一覧で、すべてpermission_view.all == true なもの、が取得できる。
Ruby で実装する際は、自作のマッパーに ActiveSupport::Callbacks を入れて、いわゆるEntry#after_save でSetting#recent_updated を書き換えるようにした。(本当は_bulk_update を使って両方更新されることを1トランザクションであつかった方がいいのかもしれないが、ブログの更新情報が1回分欠落したって誰も文句は言わないと思う)。