ではフォロー中のユーザの発言も、自分のタイムラインに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除く)のときに次が表示されてしまいますが、発言が空になるだけだし、まあいいか(をい)。