ポリモフィックアソシエーション

1つのオブジェクトを複数のオブジェクトと関連付けることが出来る。
例えば、SNSなどである、コメントや足あと。どちらも、「あるユーザーが何に対して」コメントする、
足あとをつけるという構造になっている。

ここを抽象化したいと考えた。よって「誰が何に対して」という部分をあらわすFlagモデルを作成
することにする。


しかしここで問題なのは、Flagの内、どのレコードでも、「誰が」の部分は例えばuser_idになるだろう。
しかし、「何に」の部分は、それがコメントに対しての場合もあれば、足跡に対しての場合もある。
足跡の場合、userは沢山の足跡レコードを持っていて、その中の一つの足跡が「何に」の対象になる。
この時、「誰が」の部分は「足跡をつけたユーザーになる」。
決して、「誰が誰に」という関係にはならないことに注意したい。あくまで、足跡レコードに対して
フラグがつく。


さて、話を戻すと、何にの部分に何種類もあるため、Flagテーブルだけでは上手く扱えないように思える。
何故なら、「何に」の部分に日記(id = 1)と、足跡(id = 1)が入ってしまったら、そのid = 1が、一体
どのテーブルのレコードを指すのか全く分からなくなってしまうからである。場合によっては、Duplicate
エラーがDBレベルで起こる。


これを解決してくれるのがポリモフィックアソシエーションである。
簡単に言えば、関連先レコードのidと、その関連先のモデルのタイプ(モデル名)をセットで持たせると
言うことだ。以下、実装を示す。

Migration
  class CreateFlag < ActiveRecord::Migration
    
    def self.up 
      create_table :flags do |t|
        t.column :user_id,        :integer
        t.column :flaggable_id,   :integer
        t.column :flaggable_type, :string
      end
    end
    
    def self.down
      drop_table :flags
    end
 
  end
Model
  class DiaryComment < ActiveRecord::Base
    belongs_to :diary
    has_many :flags, :as => :flaggable    #厳密に言えばhas_oneか
  
    validates_presence_of :content
  end
 
 class Flag < ActiveRecord::Base
   belongs_to :flaggable, :polymorphic => true
   belongs_to :user
 end
Controller
※テスト用なので汚い。Flag処理はFlagモデル内にまとめるべき

  def create_comment
    @diary = Diary.find_by_id(params[:id])
    @user = current_user
    @diary_comment = DiaryComment.new(params[:diary_comment])
    @seen_user = User.find_by_id(params[:user_id])

    if @diary_comment.valid?
      flag = Flag.new
      @user.flags << flag
      @diary_comment.flags << flag
      @user.diary_comments << @diary_comment
      @diary.diary_comments << @diary_comment

      flash[:notice] = 'コメントを書き込みました'
      redirect_to :controller=>"account",:action => 'home',:id => @seen_user
    else
      render :action => 'new_comment'
    end
  end
■View

  <% if @diary.diary_comments.size > 0 -%>
  <tr><td align="center">コメント書いた人たち</td></tr>
   <% for comment in @diary.diary_comments -%>
     <tr><td>
       <%= comment.flags.first.user.nickname if comment.flags.first && 
     comment.flags.first.user %>
     </td></tr>
   <% end -%>
 <% end -%>

追記

上記のコード(DiaryCommentモデル)にある「#厳密に言えばhas_oneか」はうそ。has_oneにすると、大変無駄になる。
has_oneの場合、Diaryに対してDiaryCommentがあり、そこに複数のフラグがあり、各フラグに対して、コメントの書き込み主(user)がくっついていることになる。

何が問題?

すると、検索効率が非常に悪くなる。例えば、has_oneの場合コメントの書き込み主を得る場合は次のようになる。

users = DiaryComment.find(1).flags.map{|flag| flag.user}

それに、コメントをユーザーの何らかのデータで処理する場合は次のように多段のJOINを使うことになる。多段のJOINはベンチとると結構重いので困りもの。

DiaryComment.find(
                   :all,
                   :conditions => ["users.something = ?",User::SOMETHING]),
                   :include => {"flag" => "user"}
                 )
has_manyなら?

当然以下のようにいける。

users = DiaryComment.find(1).flag.users

DiaryCommentからみて、自分を参照しているフラグを探す手間が、1回に減る。
よってhas_manyであるほうがいい。