Follow中のユーザの発言をmixして表示する

ではフォロー中のユーザの発言も、自分のタイムラインにmixして表示できるようにしてみましょう。

コントローラの変更

Userコントローラに以下のhomeメソッドを追加します。ついでに、1ページに表示される発言を最大12件に絞っておきます。また、各ユーザの発言のみを表示するよう、indexも変更しておきます。

  def index
    redirect_to(:controller => :account, :action => :login) and return if !logged_in?
    if params[:id]
      @user = User.find_by_login(params[:id])
      render :text => "not found" and return unless @user
      render_user(@user)
    else
      redirect_to(:controller => :user, :action => :home)
    end
  end

  def home
    redirect_to(:controller => :account, :action => :login) and return if !logged_in?
    @user = current_user
    render_user(@user, *current_user.followees)
  end
<snip>

protected
  def render_user(*users)
    @users = users
    @limit = 12
    @page = params[:page] ? params[:page].to_i : 0
    offset = @page * @limit
    offset =  0 if offset < 0
    @statuses = Status.find_all_by_user_id(@users.map{|u|u.id}, :order => 'created_at DESC',
                                           :limit => @limit, :offset => offset)
    render :action => :index
  end

ビューに渡す変数は以下の通り。

変数 内容
@user 表示対象のユーザ
@users 発言をmixするユーザの配列
@statuses mixした発言の配列
@page ページ数
@limit 1ページの最大発言数

まずhomeでは、ログイン中のユーザ(=@user)と current_user.followees を引数として、render_userを呼び出します。indexでは@user のみを引数としています。

次にrender_userでは、@usersに渡された配列の一覧を代入し、find_all_by_user_idでmixした発言を一度に検索します。
userそのものでは検索できないので、いったん @users.map{|u| u.id } でidの配列に変換し、検索対象としています。(これくらいActiveRecord内でやってくれても良いのになあ。)
また、ソート順や上限数、ページ数に応じた開始位置も指定しています。

こうして取得した情報をもとに、ビュー index.rhtml をレンダリングします。

ビューの変更と負荷の軽減

ミックスして発言を表示する部分は、次のように書き換えておきます。

<% users = @users.inject({}) {|h,u| h[u.id] = u; h } %>
<% @statuses.each do |s| %>
  <% u = users[s.user_id] %>
  <%= link_to(icon_tag(u, 32, :align => "left") + "<strong>" + h(u.login) + "</strong>",
                       :action => :index, :id => u.login) %>
  <%= s.status%> <small>(<%= time_diff(s.updated_at) %>)</small>
  <% if current_user == u %>
    <%= link_to "[x]", {:controller => :status, :action => :delete, :id => s.id}, :confirm => '本当に削除しますか?' %>
  <% end %>
  <hr style="clear:left">
<% end %>

1行目のusers や、3行目のu = users[s.user_id]というのは何をやっているのか?

素直に書くと s.user でユーザを取得したくなりますが(実際それでも動く)、それだとループが1度回るたびにStatus#user_idを元にDBからUserを検索することになるため、負荷が高くなってしまいます。既にコントローラから@usersとしてユーザの情報は受け取っていますから、これを使った方が効率的です。


そこで、まず inject を使って、@usersに含まれるユーザをIDから引くためのハッシュを作ります。
{ ID => User } (e.g. {1=><#User id=1>, 3=><#User id=3>, ... } )という感じです。(injectについてはここが参考になる。ruby 1.8.6なのでtapないけど…。)


あとは自分で users[s.user_id] と自分でs.user_idからUserを取得してあげれば、s.userと違ってDB検索は走らないため、負荷が低減できます。


(RoR側でキャッシュしてたりすれば良いのになあ…で一定数超えたらとかGCがかかる時とかにキャッシュも捨てるとか。前者はローカルDBの場合はDBのキャッシュとかぶるし、後者だとGC前に呼ばれるコールバックなんて無いみたいだけど。。)


なお、icon_tag というのはアイコンを表示するためのタグを生成するヘルパメソッドで、app/helpers/user_helper.rb 内に自分で定義したものです。


ついでに、ページを切り替えるためのリンクも用意しておきましょう。

<div id="page">
<% if @page > 0 %>
  <%= link_to "[ ← 後へ ]", :page => @page - 1 %>
<% end %>
<% if @statuses.length == @limit %>
  <%= link_to "[ 前へ → ]", :page => @page + 1 %>
<% end %>
</div>

これだけだと@limitの倍数(0除く)のときに次が表示されてしまいますが、発言が空になるだけだし、まあいいか(をい)。


ここまでのソース

一応オープンソース
monologue_004.tar.bz2