Ruby on Rails:ポリモーフィック関連付け

下記のようなモデルを実装するとする。

image.png

上記は、bookモデルとreportモデルはそれぞれcommentモデルと関連があり(本と報告書に対してコメントする機能)、commentは共通モデルで対応することを想定している。

そんな時に使えるのが、ポリモーフィック関連付け。

ポリモーフィック関連づけ

ポリモーフィック関連付けを使うと、次のことができるようになる。

ある1つのモデルが、他の複数のモデルに属していることを1つの関連付けで表現する

ポリモーフィック関連付け

上で検討したER図では、bookモデルとreportモデルは共通のcommentモデルで関連を持ちたいので、ポリモーフィック関連付けの出番ということになる。

commentモデルをbookモデルとreportモデルの両方に従属させたい時には、モデルを次のように宣言する。

class Comment < ApplicationRecord
  belongs_to :imageable, polymorphic: true
end

class Book < ApplicationRecord
  has_many :comments, as: :imageable
end

class Report < ApplicationRecord
  has_many :comments, as: :imageable
end

belongs_toポリモーフィックで宣言すると、任意のモデルから利用可能なインタフェースとして設定したとみなせる。

これのおかげで、@book.comments@report.comments といった形で、各モデルのインスタンスから、コメントを参照できるようになる。

ポリモーフィックなモデルを生成する

上で示したモデルを作ってみる。下記コマンドでマイグレーションファイルのテンプレートを作成する。

% rails g migration CreateComments

次にマイグレーションファイルに、属性、ポリモーフィックと外部キーを追加する。

class CreateComments < ActiveRecord::Migration[6.1]
  def change
    create_table :comments do |t|
      t.text  :content
      t.references :imageable, polymorphic: true
      t.belongs_to :user, foreign_key: true

      t.timestamps
    end
  end
end

マイグレーションファイルを作ったら、モデルを生成する。

% rails db:migrate

次に、コントローラの設定をしていく。

# app/models/comment.rb
class Comment < ApplicationRecord
  ...
  belongs_to :user # 外部キー用
  belongs_to :imageable, polymorphic: true # ポリモーフィック用
  ...
end

# app/models/book.rb
class Book < ApplicationRecord
  ...
  has_many :comments, as: :imageable # ポリモーフィック用
  ...
end

# app/models/report.rb
class Report < ApplicationRecord
  ...
  has_many :comments, as: :imageable # ポリモーフィック用
  ...
end

# app/models/user.rb
class User < ApplicationRecord
  ...
  has_many :comments, dependent: :destroy # ユーザ消えたらコメントも削除
  ...
end

試しに操作してみる。

% rails c
irb(main):001:0> book = Book.first
   (1.0ms)  SELECT sqlite_version(*)
  Book Load (0.1ms)  SELECT "books".* FROM "books" ORDER BY "books"."id" ASC LIMIT ?  [["LIMIT", 1]]      
irb(main):002:0> book.comments.create(id:1,content:"test",user_id: 1)
  TRANSACTION (0.1ms)  begin transaction
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]                              
  Comment Create (1.3ms)  INSERT INTO "comments" ("id", "content", "imageable_type", "imageable_id", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?)  [["id", 1], ["content", "test"], ["imageable_type", "Book"], ["imageable_id", 1], ["user_id", 1], ["created_at", "2022-09-08 14:13:47.757347"], ["updated_at", "2022-09-08 14:13:47.757347"]]
  TRANSACTION (0.5ms)  commit transaction

作ったデータを確認してみる。

% rails db
sqlite> select * from comments;
1|test|Book|1|1|2022-09-08 14:13:47.757347|2022-09-08 14:13:47.757347

参考