Followを実装

id:NeoCat:20080525 から作り始めたTwitterもどき「monologue」に、他のユーザのFollow機能を実装していきます。


まずは、ActiveRecordを使ってユーザ間の関連を取得できるようにします。

テーブルの作成

NeoCat:20080525:1211652175 に書いたように、Followテーブルを使ってFollowしているユーザを管理します。

まずモデルと、それに対応するテーブルを作成します。

% script/generate model follow

そして、生成されたmigrationファイル( 00x_create_follows.rb )を編集し、create_tableメソッドのブロック内に以下を追記。

      t.column :user_id,         :integer
      t.column :followee_id,     :integer

user_idはFollow元のユーザのidを示し、followee_idはフォローされている側のユーザのidを示します。
なお、followeesというのは「フォローされている人」という意味です。逆の「フォローしている人」はfollower。
employee と employer と同じ関係です。


編集後、以下のコマンドで実際にテーブル作成。

% rake db:migrate

UserモデルとFollowモデル間の関連を記述(フォロー元→フォロー先)

次にFollowモデルおよびUserモデルに互いの関連を書きます。ここでは、@user.followees で@userがフォロー中のユーザを配列で取得できるようにしてみましょう。

まずapp/models/user.rbに以下を追記。

  has_many :follows,   :dependent => :destroy
  has_many :followees, :through => :follows, :class_name => 'User'

さらに、app/models/follow.rbに以下を記述します。

  belongs_to :followee,    :foreign_key => :followee_id, :class_name => 'User'

  validates_associated     :followee
  validates_uniqueness_of  :followee_id, :scope => [:user_id]


最初の has_many :follows〜 で UserとFollowが1:nの関係にあること、またUserが削除されたらFollowも削除することを示しています。
これで、@user.follows でそのユーザに対応する Follows の配列が取得できるようになります。


さらに、次の has_many :followees, :through => :follows, :class_name => 'User' は、先ほどのfollowsの関連を通して、User同士がn:nの関係にあることを示しています。
関連がUser同士であるため、クラス名_id という規約に沿った user_id という名前をFollowsテーブルのフィールドにつけることが出来ません。そこで、クラス名を:class =>で明示しておきます。


これに対応するFollow側の宣言がbelongs_to :followee〜です。Userクラスのfollowee(s)に属しており、user_id(規約による) → followee_id という対応付けを行うことを示しています。


その後のvalidationについては、followeeが正しい関連であることのチェック、および同じuser_idの中ではfollowee_idが重複しないことのチェックです。( validates_uniqueness_ofについての説明

以上で、@user.followees でフォロー中のユーザが取得できるはずです。

コントローラを書く

% script/generate controller follow

でコントローラのひな形を生成し、以下を記述します。ログイン中のユーザに対して、http://localhost:3000/follow/new/<ユーザ名> でフォローを追加、newの代わりにdeleteでフォロー解除できるようにします。

class FollowController < ApplicationController
  def new
    redirect_to(:controller => :account, :action => :login) if !logged_in?
    user = User.find_by_login(params[:id])
    render :text => "not found" and return unless user
    follow = Follow.new(:followee_id=> user.id)
    begin
      current_user.follows << follow
      flash[:notice] = "#{user.login} をfollowしました。"
      redirect_to(:controller => :user, :action => :index, :id => params[:id]) and return
    rescue
      flash[:notice] = follow.errors.full_messages
      flash[:notice] = "エラー : followに失敗しました。" if @flash[:notice].to_s == ""
    end
    redirect_to(:controller => :user, :action => :index)
  end

  def delete
    user = User.find_by_login(params[:id])
    follow = params[:id] && Follow.find(:first, :conditions => ["followee_id = ?", user.id])
    if follow && follow.user_id == current_user.id
      follow.destroy
      flash[:notice] = "#{params[:id]} のfollowを解除しました。"
    end
    redirect_to(:controller => :user, :action => :index, :id => params[:id]) and return
  rescue
    flash[:notice] = "エラー : follow解除に失敗しました。"
    redirect_to(:controller => :user, :action => :index)
  end
end

試しにビューを書いてみる

ユーザ情報のビュー(app/views/user/index.rhtml)に以下を記述して、ちゃんとFollowしているユーザが取得できることをチェックしてみましょう。

<b>このユーザがfollow中のユーザ:</b><br>
<% @user.followees.each do |u| %>
  <div class="follow"><%= link_to(h(u.login), :action => :index, :id => u.login ) %></div>
<% end %>

上記で、ユーザのlogin名と、そのユーザの情報ページへのリンクを表示しています。