Rails で MongoDB を使ったアプリを作ってみてるわけですが、親子関係になってるテーブルでデータを集計したかったのでやってみました。

やりたかったことは、「プロジェクト」に「ユーザー」が「段階的」に「出資金」を出せるので、そのプロジェクト毎に出資金の合計値を出したい、とかそんな感じです。

テーブルはこんな感じ。

  • User (id, name, …)
  • Project (id, name, …)
  • Rank (id, amount, …)
  • Investment (id, project_id, user_id, rank_id, …)

この、「段階的」にを表すRankが曲者で、出資金額はここを見ないと取れないわけです。Investmentのrank_idをたどってRankを見ないと出資金額が分からない。が、 MapReduce で使う JavaScript で書いた関数からは自ドキュメントしか見えない!!1

仕方ないので、 JavaScript で Switch 文を使いました。まーRankが3ランクくらいしかないので力技で。

というわけでInvestmentの中身はこんな感じになりました。

class Investment
  include Mongoid::Document
  include Mongoid::Timestamps
  belongs_to :user
  belongs_to :project
  belongs_to :rank

  def total_amount
    map = <<-MAP
      function() {
        var amount = 0;
        switch (this.rank_id.toString()) {
          case "#{Rank.where(:rank => 1).first.id}":
            amount = "#{Rank.where(:rank => 1).first.amount}";
            break;
          case "#{Rank.where(:rank => 2).first.id}":
            amount = "#{Rank.where(:rank => 2).first.amount}";
            break;
          case "#{Rank.where(:rank => 3).first.id}":
            amount = "#{Rank.where(:rank => 3).first.amount"};
            break;
        }
        emit(this.project_id, {amounts: amount});
      };
    MAP

    reduce = <<-REDUCE
      function(key, values) {
        var sum = 0;
        values.forEach(function(doc) {
          sum += doc.amounts;
        });
        return {amounts: sum};
      };
    REDUCE
    collection.map_reduce(map, reduce, :query => {:ranking_id => self.ranking.id}).find().first()["value"]["amounts"].to_i
  end
end

で、このtotal_amountをView/Controller側から使えば集計結果が取れます。こんな感じ。

@project = Project.find(params[:id])@project.investments.first().total_amount

Switch 文でかなり無理やりですが、1:Nの親子関係になってるテーブルでも集計できました。もっと簡単な方法やいい方法があればぜひ教えてください。

たぶん正規化のし過ぎなんだろうなー。参照じゃなくて埋め込みにしちゃえばSwitch 文は使わなくてよくなりそう。

参考資料