title-logo

Kajaeru コードリーディング

December 05, 2014

この記事は「よちよち.rb Advent Calendar 2014」5日目の記事です。

昨日の記事は bonbon0605 さんによる「第3回MetaNightに参加してきました」でした。この MetaNight には私も第1回から参加していて、毎回とても楽しいです。英語の本、更にメタプログラミングを扱った本ということで、ちょっと敷居が高く感じられるかもしれませんが、1回に読む分量はほどほどで、それ以上に内容や Ruby のことについての話をしている (もちろん日本語で) ことが多いので、英語が得意でない方も楽しめる会となっていると思います。その分、最後まで読破できる日はだいぶ先のことになりそうですが…

Kajaeru

おだいさんの2日目の「Yochiyochi Committers Who’s Who in 2014」でも少し触れられていましたが、よちよち.rb では Rails を使って Kajaeru というサービスを開発しています。 Kajaeru とは、よちよち.rb の RubyKaja を決めるために、決めるために、えーと、何だろう、投票?投票とかできるやつなのかな…うーん、誰にどうやって投票するんだろう…候補はどうやって選ぶのかな…

ごめんなさい!実は Kajaeru のこと全然知りません!よちよち.rb に参加しはじめてまだ日が浅く、おまけに Ruby コミュニティに参加したのもよちよちが初めてなので、RubyKaja というのもはじめて知りました。

…ということで、ソースコードを読んで、Kajaeru がどのようなサービスなのか見てみることにします。「コードリーディング」というほどちゃんとしたものにはならないかもしれませんが、他のよちよち新規メンバーで、Kajaeru に contribute したい人のとっかかりになればいいなと思います。間違いがあったら教えてね。

README

とりあえず README を眺めてみます。
https://github.com/yochiyochirb/kajaeru

事前準備

順番に見ていきます。まずは「事前準備」から。

Kajaeruのログイン機能はGithub APIのOauthを利用するため、こちらより、Developer applicationsに登録し、Kajaeruのための Client ID と Client Secret を取得してください。

とあります。なるほど。どうやらユーザーの認証には GitHub の OAuth を利用しているようです。そのため、動かす際には GitHub に Developer application を登録し、Client ID と Client Secret を取得する必要があるようですね。「設定内容サンプル」には、Developer application に登録するときのサンプル情報も書いてあります。ここはそのまま入力すればよいでしょう。

環境構築

構築の手順が、実行するコマンドの流れとして記載してあります。ほぼ Rails アプリケーションを構築する定型の流れですが、以下のところでなにか特別なことをやっているようです。

bundle exec rails runner lib/batch/insert_to_members_from_github_contributors.rb

rails runner は、Rails の環境を読み込んだ上で、指定した Ruby のスクリプトを実行できるコマンドです。ではこの insert_to_members_from_github_contributors.rb というのは何をするためのものなんでしょう?名前からすると、「GitHub の contributers から members に挿入する」ものと想像できます。

insert_to_members_from_github_contributors.rb

この中を見てみると、

res = open('https://api.github.com/repos/yochiyochirb/meetups/stats/contributors')

で、yochiyochirb/meetup の contributer 情報を取得しているようです。情報は JSON レスポンスとして返ってきて、それを以下のように処理しています。

contributors = ActiveSupport::JSON.decode res.read
contributors.each do |contributor|
  author = contributor['author']
  next if Member.find_by(uid: author['id'].to_s)
  member = Member.create!(
    nickname: author['login'],
    provider: 'github',
    uid: author['id'].to_s,
    image: author['avatar_url'])
end

contributer をそれぞれ、Member というクラスのオブジェクトとして生成しているようです。ただ、ファイル名からしても、取得した contributer を Member#create! でデータベースに保存することがここでの目的でしょう。つまり、Member というのは Kajaeru の重要なモデルの1つだと推測できます。

README のそれ以降は環境変数の設定や Heroku へのデプロイ tips なので省略して、ちょうどモデルの話が出てきたところなので、次はモデルを見ていきます。

ここまでのまとめ

Model

ここから、Kajaeru の本体のコードを見ていきます。まずはモデルです。ほとんどの場合、モデルはアプリケーションによって「実現したいこと」の中心となる登場人物 (もちろん人とは限りませんが) を抽象化したものなので、モデルの全体像を大雑把にでも把握しておくと、アプリケーション全体の関心事が見通せるようになると思います。

まずはさらっと app/models の中を見てみます。member.rbvote.rb があるので、Kajaeru には、以下の2つのモデルがあるようです。

Member モデル

Member はやっぱりありましたね。構築手順の中で、このモデルに yochiyochirb/meetup の contributer を登録していました。しかし、member.rb を見ても、いくつかバリデーションが定義されているだけです。当然、モデルは ActiveRecord を継承しているので、このモデルがどんな属性を持つのかは、データベースのスキーマを見てみる必要があります。

db/schema.rb

  create_table "members", force: true do |t|
    t.string   "nickname",   null: false
    t.string   "provider",   null: false
    t.string   "uid",        null: false
    t.string   "image",      null: false
    t.datetime "created_at"
    t.datetime "updated_at"
  end

created_atupdated_at を除けば、Member は4つの属性を持っているようです。それぞれがどんなものなのかを知るには、先ほどの insert_to_members_from_github_contributors.rb で登録していた内容が参考になりそうです。

  member = Member.create!(
    nickname: author['login'],
    provider: 'github',
    uid: author['id'].to_s,
    image: author['avatar_url'])

登録するデータの元は GitHub の contributer なので、ここから推測できるのは、

まだ不明確なところが残ってますが、何となく概要は掴めました。あとは実際に一度動かして中身を確認すれば一発でわかるでしょうが、とりあえずこのまま読み進めていきます。

Vote モデル

次のモデルは Vote です。名前からして投票をモデル化したものでしょう。もし全然違ったら詐欺案件です。普通に考えると、Vote モデルの1つ1つがそれぞれ1票の投票を表すものとして定義されていると思います。

db/schema.rb

  create_table "votes", force: true do |t|
    t.text     "comment"
    t.integer  "voted_member_id",  null: false
    t.integer  "voting_member_id", null: false
    t.datetime "created_at"
    t.datetime "updated_at"
  end

member.rb と違って、 vote.rb にメソッドが定義されているので、そっちも見ておきます。

app/models/vote.rb

  def self.total
    vote_counts_and_users = group(:voted_member_id).count(:voted_member_id).sort{|a,b| b[1]<=>a[1]}
    vote_counts_and_users.inject([]) do |vote_totals, vote_count_and_user|
      # vote_count_and_user => [5, 10]
      user_id,total = vote_count_and_user
      member  = Member.where(id: user_id).first
      vote_totals.push({
                      nickname: member[:nickname],
                      image:    member[:image],
                      total:    total,
                      comments: where(voted_member_id: user_id).pluck(:comment)
                      })
    end
  end

total というクラスメソッドが定義されています。名前の感じからすると合計を求めるものでしょうか。はじめてコードリーディングっぽいコードが出てきたので、少し細かく見てみましょう。

vote_counts_and_users = group(:voted_member_id).count(:voted_member_id).sort{|a,b| b[1]<=>a[1]}

ここ、たったの1行ですが、慣れていないので結構戸惑いました。この行でやっていることは、

  1. データベースの votes テーブルのデータを、voted_member_id カラムの値をキーとしてグループ化する
  2. それぞれのグループの voted_member_id カラムのレコード数をカウントする (つまり各グループのレコード数のカウント)
  3. カウントした結果を、{ グループのキー : レコード数, グループのキー : レコード数, … } のハッシュとして返す
  4. ハッシュをレコード数でソートする
  5. ソート結果を vote_counts_and_users に代入する

となります。

ただ、「groupActiveRecord 由来のメソッドで…」というように1つ1つ見ていくよりは、groupcount のメソッドチェーンを、グループ化してレコード数をカウントするため Rails の1つのまとまったイディオムとして覚えてしまう方が良い気がします。というのも、これらのメソッドチェーンでは、メソッド呼び出しに対応した SQL が即時実行されるわけではなく、単一の SQL クエリが遅延実行されるからです。この group などのメソッドは ActiveRecord のクエリインターフェースと呼ばれ、ActiveRecord::Relation のオブジェクトを返してメソッドチェーンに組み込むことで、その遅延実行を実現しているとのことでした。正直、これを書いている私も勉強しはじめたばっかりなのであまり詳しいことはわかりません…

Ruby 的には、返ってきたハッシュをソートするときにブロックで渡しているのが |a,b| b[1]<=>a[1] であるのが興味を引くかもしれません。やりたいことはハッシュのソートですが、ソート条件では配列のインデックスを指定しています。これは、ハッシュのソートは各キーと値のペアを配列としてソートするからです。なので当然、戻り値は配列で、vote_counts_and_users に代入されるのも配列の配列になります。そして、ここでグループのキーとなっているのは voted_member_id で、わざわざこうやって数を集計しているぐらいだから、間違いなくこれは投票されたメンバーの ID を示しているでしょう。したがって、結論としては、vote_counts_and_users に格納されるのは [[メンバーの ID, 得票数], [メンバーの ID, 得票数], [メンバーの ID, 得票数], …] なものであろうと考えられます。

次にいきましょう。

    vote_counts_and_users.inject([]) do |vote_totals, vote_count_and_user|
      # vote_count_and_user => [5, 10]
      user_id,total = vote_count_and_user
      member  = Member.where(id: user_id).first
      vote_totals.push({
                      nickname: member[:nickname],
                      image:    member[:image],
                      total:    total,
                      comments: where(voted_member_id: user_id).pluck(:comment)
                      })
    end

ここでは、inject を効果的に使って、取得した配列の配列を、

{
  nickname: メンバーのニックネーム,
  image: メンバーの画像,
  total: 得票数
  comments: 投票コメントの配列
}

という別の形式のハッシュに組み立てて、それを配列に突っ込んで返しています。そのとき、メンバーの情報は、先ほど取得したメンバー ID で紐付けて members テーブルから引っぱってきています。個人的には、comments に入る内容を取得しているところの pluck というメソッドが目に付きました。はじめて見たんですが、「こんなのもあるんだ、超便利、超勉強になる!」とちょっと感動しました。

この total メソッドがどこで使われるかはまだわかりませんが、ここでやりたいことは大体掴めましたね。

ここまでのまとめ

続く…

さて、お時間の方が残り少なくなってまいりましたので、この辺で「第1回 Kajaeru コードリーディング」を終わりにしようと思います。いや、続きものにするつもりは全然なかったんですが、予想よりめちゃめちゃ長くなってきたので…なので、コントローラーやビューを見ていくのはいつかまた次の機会ということで。誰か続きを書いてくれたりすると嬉しいですね!

それにしても、Rails はチュートリアルを途中までやっただけで、あとは「パーフェクト Ruby on Rails」をちょろっと読んだだけの自分でも一応コードを読んでいけるのは、Rails にしっかりとした規約があって、そして Kajaeru もちゃんとそのレールに沿って開発されているからだと思います。おーこれが Rails のすげーとこか!と改めて認識できるよい機会になりました。

「よちよち.rb Advent Calendar 2014」明日はドアラさんの「俺のよちよちrbヒストリー」です!