カテゴリー:
Rails
タグ:
 Rails モデルの継承 polymorphic

このエントリーをはてなブックマークに追加
更新日時:
2014年07月14日(月)
作成日時:
2013年07月17日(水)

前の記事 / 次の記事

似たようなモデルを扱っているからまとめてみたい気がするんだけど、
単一テーブル継承(STI)だとうまくいかなくて、

かといってガチでモデルを継承するとかいう危なそうなことはやりたくなくて
っていう時はポリモーフィックアソシエーションで上手くいくかも知れない。

参考ページ

何が出来るのか?

参考ページで全て解決なんだけど、一応自分でもまとめ。

例えば、「日記」と「本」と「お店」があって、それぞれに日記の感想として、
本を読んだ感想として、お店に行った感想としてコメントを付けたいような場合。

次のようなコードを書けばそれは出来るけど、、、

class Dialy < ActiveRecord::Base
  has_many :comments
end

class Book < ActiveRecord::Base
  has_many :comments
end

class Shop < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :dialy
  belongs_to :book
  belongs_to :shop
end

このやり方だとコメントを付ける対象が増えれば増えるほど
Commentモデルの belongs_to がどんどん増えていくからなんか微妙。

それに belongs_to 増やすだけならまだしも対象が増える度に
テーブルのカラム増やすのとかなんかキモいしそのタイミングでバグらせる自信がある。

かといって、

class Dialy < ActiveRecord::Base
  has_many :dialy_comments
end

class Book < ActiveRecord::Base
  has_many :book_comments
end

class Shop < ActiveRecord::Base
  has_many :shop_comments
end

class DialyComment < ActiveRecord::Base
  belongs_to :dialy
end

class BookComment < ActiveRecord::Base
  belongs_to :book
end

class ShopComment < ActiveRecord::Base
  belongs_to :shop
end

とかするのもなんか微妙な気がする。
将来的に修正が入った時にShopCommentだけ修正し忘れてバグらせる自信がある。

そこで、ポリモーフィックアソシエーションを使うと、

class Dialy < ActiveRecord::Base
  has_many :comments, :as => :resource
end

class Book < ActiveRecord::Base
  has_many :comments, :as => :resource
end

class Shop < ActiveRecord::Base
  has_many :comments, :as => :resource
end

class Comment < ActiveRecord::Base
  belongs_to :resource, :polymorphic => true
end

と書くことが出来る。

やり方

このアソシエーションの肝はテーブルをつくる時にRailsの規約に従ったカラムをつくること。
そのカラム名は resource_idresource_type
(※ 必ず resource_id と resource_type である必要はない。=> 後述

今回のケースでは、まずcommentsテーブルのカラムとして、
resource_idresource_type という二つのカラムを定義する。
他のカラムは好きにつくっていい。

class CreateComments < ActiveRecord::Migration
  def change
    create_table :comments do |t|
      t.integer :resource_id
      t.string  :resource_type
      t.string  :nick_name
      t.string  :body

      t.timestamps
    end
  end
end

とか。
次に、Commentモデルでポリモーフィックアソシエーションを使うことを宣言する。

class Comment < ActiveRecord::Base
  belongs_to :resource, :polymorphic => true
end

そして、Commentを付けたいモデルで、
Commentモデルとはポリモーフィックアソシエーションで関連付けられていることを宣言する。

# :as => :resource を指定
class Dialy < ActiveRecord::Base
  has_many :comments, :as => :resource
end

class Book < ActiveRecord::Base
  has_many :comments, :as => :resource
end

class Shop < ActiveRecord::Base
  has_many :comments, :as => :resource
end

こうすることで次のようなコードを書くことが出来るようになる。

# Dialy, Book, Shop はそれぞれ name というカラムを持つとして、

dialy = Dialy.create({name: "今日の出来事"})
dialy.comments.create({nick_name: "朝日奈",body: "いいと思う。"})

book = Book.create({name: "夜のピクニック"})
book.comments.create({nick_name: "如月", body: "面白かった。"})
book.comments.create({nick_name: "片桐", body: "つまらなかった。"})

shop = Shop.create({name: "ラーメン小池"})
shop.comments.create({nick_name: "清川", body: "おいしかった。"})
shop.comments.create({nick_name: "館林", body: "普通。"})
shop.comments.create({nick_name: "藤崎", body: "激マズ。"})

dialy.comments.size #=> 1
book.comments.size  #=> 2
shop.comments.size  #=> 3

dialy.comments.each do |comment|
  puts comment.nick_name + ":" + comment.body
end
#=>
  朝日奈:いいと思う。

book.comments.each do |comment|
  puts comment.nick_name + ":" + comment.body
end
#=>
  如月:面白かった。
  片桐:つまらなかった。

shop.comments.each do |comment|
  puts comment.nick_name + ":" + comment.body
end
#=>
  清川:おいしかった。
  館林:普通。
  藤崎:激マズ。

Comment.all.each do |comment|
  puts comment.resource.name
end

#=>
  今日の出来事
  夜のピクニック
  夜のピクニック
  ラーメン小池
  ラーメン小池
  ラーメン小池

更に

カラム名は resource_id と resource_type でなければいけない必要はなくて、
例えば parent_id と parent_type であったり、group_id と group_type であったりしてもいい。

その場合はそれぞれ

belongs_to :parent, :polymorphic => true
belongs_to :group,  :polymorphic => true

has_many :comments, :as => :parent
has_many :comments, :as => :group

と定義すればいい。

また、ポリモーフィックアソシエーションは1つのモデルに複数定義することもできる。
例えば次のように書いてもちゃんと機能する。

class CreateComments < ActiveRecord::Migration
  def change
    create_table :comments do |t|
      t.integer :resource_id
      t.string  :resource_type
      t.integer :category_id
      t.string  :category_type
      t.integer :sub_category_id
      t.string  :sub_category_type
      t.string  :nick_name
      t.string  :body

      t.timestamps
    end
  end
end

class Comment < ActiveRecord::Base
  belongs_to :resource,     :polymorphic => true
  belongs_to :category,     :polymorphic => true
  belongs_to :sub_category, :polymorphic => true
end

class Dialy < ActiveRecord::Base
  has_many :comments, :as => :resource
end

class Category < ActiveRecord::Base
  has_many :comments, as: => :category
end

class SubCategory < ActiveRecord::Base
  has_many :comments, :as => :sub_category
end

こうすることで、例えば、

@book = Book.find_by(name: "レッドデータガール")
@book.comments
#=> 本「レッドデータガール」に対するコメント

@category = Category.find_by(name: "ラノベ")
@category.comments 
#=> カテゴリー「ラノベ」に対するコメント

@sub_category = SubCategory.find_by(name: "学園もの")
@sub_category.comments
#=> サブカテゴリー「学園もの」に対するコメント

のように、1つのコメントが複数のグループに属し、
それぞれのグループごとにコメントのコレクションを取得できるようになる。

コメント側からコレクションを取得する場合は、
where句で検索をかけてそれぞれのグループを横断して取得することもできる。

@comments = Comment.where(category_id: 2, sub_category_id: 3)