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であるほうがいい。