Railsのポリモーフィック関連を活用したモデル実装方法（Xクローンの通知機能を例として）

Railsの学習のために、Xのクローンアプリを作成しましたが、こちらの通知機能の実装に初めて**ポリモーフィック関連**を使ってみたので、実装方法などを備忘録のために残しておきたいと思います。


:::note warn
Railsの初学者のため、誤った内容を含む可能性もあります。
間違っている部分がある場合はご指摘いただけると助かります。
:::

# 今回実装を目指す機能

この記事では、Xのクローンアプリを例に、ポリモーフィック関連を活用したモデルの実装を考えたいと思います。

前提として、以下の機能を持つXクローンアプリがあるとします。

- ユーザー登録ができる
- 各ユーザーはツイートを投稿できる
- 各ユーザーは他のユーザーをフォローすることができる(自分自身はフォローできない)
- 各ツイートに対して、いいね、リツイート、コメントができる

![スクリーンショット 2025-03-16 17.45.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3534301/f939091d-58c9-4a3b-a8b6-9cf08671b4d0.png)


各モデルは以下のように定義しています。

<details> <summary>(長いので折りたたみ)</summary>

Userモデル
|  | テーブル名 |
|:-:|:-:|
| PK  | id  |
|   | name  |
|   | email  |
|   | introduction  |
|   | (以下色々プロフィール)  |

Tweetモデル
|  | テーブル名 |
|:-:|:-:|
| PK  | id  |
| FK  | user_id  |
|   | content  |
|   | image  |

Followerモデル
|  | テーブル名 |
|:-:|:-:|
| PK  | id  |
| FK  | followed_id  |
| FK  | follower_id  |

Likeモデル (Retweetsも同じテーブル)
|  | テーブル名 |
|:-:|:-:|
| PK  | id  |
| FK  | user_id  |
| FK  | tweet_id  |

Commentモデル
|  | テーブル名 |
|:-:|:-:|
| PK  | id  |
| FK  | user_id  |
| FK  | tweet_id  |
|  | content  |

いいね、リツイート、コメントのモデルが一体多の関係ででuserとtweetの中間テーブルになっています。

</details>


その上で、以下の要件を満たす通知機能を新たに追加したいと思います。

- 自分がフォローされた場合は、フォローしたユーザーを通知する
- 自分のツイートに対して、いいね、リツイート、コメントのアクションを他ユーザーから行われた場合は、対象の自分のツイートと、ツイートに対して行われたアクションの種類(いいね/リツイート/コメント)とアクションしたユーザーを通知する
- 自分のツイートに対するいいね、リツイート、コメントは通知しない

完成の画面イメージはこのような感じです。

![スクリーンショット 2025-03-16 17.47.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3534301/d3249321-37f4-47b9-86ea-aaa5661817f4.png)



このように、通知内容とアクションを起こした相手が通知一覧に表示されるように機能を作っていきます！

# ポリモーフィック関連を利用しないで通知モデルを作る場合はどうなるか

通知には通知対象のツイートと通知の種類、アクションしたユーザーを乗せる必要があるため、通知に関連する各モデルと通知モデルをそれぞれ個別で関連付けの設定をしてあげる必要があります。

その結果、関連付けを増やすたびに通知モデルのカラムが増えてしまう運用になってしまいます。

今後、通知する対象のモデルが増えるたびに関連付けとカラムを増やす必要があり、手間がかかりそうです。

都度設定を要するのでDRYの原則に反しますね。

他の実装方法もあるかもしれませんが(思いつかない)、どちらにしても少し煩雑な実装方法になりそうです。


# ポリモーフィック関連とは

本題です。

このように、**あるモデルが複数のモデルに属していて、その関連付けで使用する機能が共通している場合**、ポリモーフィック関連が使用できます。

ポリモーフィック関連は[Railsガイド](https://railsguides.jp/association_basics.html#%E3%83%9D%E3%83%AA%E3%83%A2%E3%83%BC%E3%83%95%E3%82%A3%E3%83%83%E3%82%AF%E9%96%A2%E9%80%A3%E4%BB%98%E3%81%91)では、以下のように解説されています。

> ポリモーフィック関連付け（polymorphic association）は、関連付けのやや高度な応用です。
> Railsのポリモーフィック関連付けを使うと、ある1つのモデルが他の複数のモデルに属していることを、1つの関連付けだけで表現できます。
> ポリモーフィック関連付けは、あるモデルを種類の異なる複数のモデルに紐づける必要がある場合に特に便利です。

初めて私が使ってみて感じたポリモーフィック関連の最大の特徴は、create_tableするときに、`[共通で関連するカラム]_type`のカラムが自動作成されることだと思います。

今回はこのポリモーフィックで共通関連するカラム名を`notifiable`とします。これでマイグレーションするとnotifiable_idだけではなく、notifiable_typeというカラムが自動作成されます。

notifiable_typeにはどのモデルに属しているかどうかの情報がモデル名(LikeやRetweet)として記録され、notifiable_idには関連先のオブジェクトのidが乗るように作ることができます。

これを活用することで、notifiable_typeで通知の種類を判定して表示内容を変える、といった運用もできます。　このモデルの設計が自動で作られるのは便利ですね。


:::note info
ポリモーフィック関連に使用するカラムは、慣例として`〇〇able`という命名にすることが多いようです。

「通知できる」「通知が発生する元」… みたいなニュアンスで-ableを使っていると解釈しています。
:::


# Xクローンの通知機能実装にポリモーフィック関連を使ってみる

それではXクローンに通知機能を実装していきたいと思います。

まずは通知モデル(`Notifications`)を作ります。

```ruby:db/migrate/20250224095232_create_notifications.rb
class CreateNotifications < ActiveRecord::Migration[7.0]
  def change
    create_table :notifications do |t|
      t.references :notifiable, null: false, polymorphic: true
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end
```

`notifiable`にポリモーフィック関連付けを適用する場合は、上記のように、notifiableに`polymorphic: true`を付与します。

こちらでdb:migrateすると、以下のようなモデルが作成されます。

```ruby:db/schema.rb
# 略
  create_table "notifications", force: :cascade do |t|
    t.string "notifiable_type", null: false
    t.bigint "notifiable_id", null: false
    t.bigint "user_id", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["notifiable_type", "notifiable_id"], name: "index_notifications_on_notifiable"
    t.index ["user_id"], name: "index_notifications_on_user_id"
  end
# 略
```

先ほど説明したように、ポリモーフィック関連付けしたnotifiableには`notifiable_id`だけではなく`notifiable_type`のカラムが作られました。


それでは通知の仕組みを作っていきたいと思います。

まずは通知モデル側の関連付けを書いていきます。

```ruby:app/models/notification.rb
class Notification < ApplicationRecord
  belongs_to :notifiable, polymorphic: true
  belongs_to :user
end
```

notifiableにbelong_toする形で設定します。ここでも`polymorphic: true`と設定します。

そして、一番大事な通知のデータが作られる仕組みを実装していきます。

今回、フォロー、いいね、リツイート、コメントされた時に必ず通知のデータが作られる仕組みを作る必要があります。

そこで、`after_create_commit`を活用して、通知に関連するデータが新規作成されたら通知のデータも一緒に作られるようにします。

通知対象とする各モデルのポリモーフィック関連付けの設定や、通知データを作る仕組みは共通化している部分も多いので、以下のモジュールを作ってそれぞれincludeさせたいと思います。

```ruby:app/models/concerns/notifiable.rb
module Notifiable
  extend ActiveSupport::Concern

  included do
    # 通知モデルとのポリモーフィック
    has_many :notifications, as: :notifiable, dependent: :destroy

    after_create_commit :create_notifications
  end

  private

  def create_notifications
    # 通知除外判定
    return if check_create_notification

    # 通知作成
    notification = Notification.create(notifiable: self, user: notification_recipient)
    notification.save!
  end
end

```

after_create_commitによって実行されるcreate_notificationsについて、自分自身へ通知は行わないので、`check_create_notification`で自分自身へのアクションは除外します。これは後で各モデルで定義します。

そして、`Notification.create`で通知のオブジェクトを作ります。notifiable_typeには関連付けされるクラス名が入るので、`notifiable: self`とします。

`notification_recipient`通知元のユーザーを指定する関数として各モデルで定義します。


あとは、ポリモーフィック関連を適用する各モデルに対して、Notifiableをincludeして、上記の`check_create_notification`と`notification_recipient`を定義すれば、通知を作成する仕組みが完成です。

いいね(Like)を例とすると、

```Ruby:app/models/like.rb
class Like < ApplicationRecord
  #追加
  include Notifiable
  

  belongs_to :tweet, counter_cache: true
  belongs_to :user

  validates :user_id, uniqueness: { scope: :tweet_id }

  private
  
  # 以下追加
  # 自分自身へのアクションかどうかの判定
  def check_create_notification
    tweet.user == user
  end

  # 通知受信者の指定
  def notification_recipient
    tweet.user
  end
end
```

リツイート、コメント、フォローも同様のような設定を行います。

以上の設定を行えば、各アクションを起こした際、Notificationにデータが追加されるはずです。

試しにいいね、リツイート、コメントなどをやってみた上でコンソールにて`Notification.all`すると…

![スクリーンショット 2025-03-16 16.58.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3534301/11da2b9c-59a7-429b-8520-62cfbd611b04.png)


無事に通知が作られているのが分かります。`notifiable_type`に、通知のモデル名が代入されていますね。


あとは以下のようにnotifiable_typeの内容によって分岐するviewを用意すれば、「通知の種類によって通知の内容・デザインを変える」といったことができます。

```slim:app/views/notifications/index.html.slim
/ 〜略〜
- @notifications.each do |notification|
  .border-bottom
    .ms-4.mb-3.mt-1.me-3
      .hstack
        - case notification.notifiable_type
        - when 'Like' then
          .fs-3.ms-1.mt-1.mb-auto
            i.bi.bi-heart-fill.text-danger
          .vstack
            = link_to user_path(notification.notifiable.user.name, tab: 'tweet') ,class: "ms-2 mt-2" do
              - if notification.notifiable.user.avater_image.present?
                = image_tag notification.notifiable.user.avater_image.representation(resize_to_fill: [30, 30]), class: "rounded-circle me-2 border"
              - else
                = image_tag "default_i.png", class: "rounded-circle me-2 border", size: '30x30'
            .ms-2.mt-2
              .hstack
                = link_to user_path(notification.notifiable.user.name, tab: 'tweet') ,class: "fw-bold text-decoration-none link-dark" do
                  = notification.notifiable.user.name
                | さんがあなたのツイートをいいねしました
            = link_to tweet_path(id: notification.notifiable.tweet.id), class:"ms-2 mt-2 text-muted text-decoration-none link-dark" do
              = notification.notifiable.tweet.content
        - when 'Retweet' then
/ 〜略〜

```

`notifiable`による関連付けにより、通知元のツイートなどのデータを引っ張ることもできています。

これで冒頭のスクリーンショットのような通知一覧表示機能が完成しました。

以上のように、ポリモーフィック関連を活用することで、通知機能のように一つのモデルが複数のモデルに共通機能内容で実装するのが容易になります。

# まとめ

- ポリモーフィック関連は、1つのモデルが複数の異なるモデルと共通した機能内容で関連付けられる場合に有効
- ポリモーフィック関連では、〇〇_id に加えて、関連元のモデル名が入る 〇〇_type を使って関連付けを行い、関連先が動的に変わるようにする


## 参考にさせていただいた記事

- [Railsガイド「ポリモーフィック関連付け」](https://railsguides.jp/association_basics.html#%E3%83%9D%E3%83%AA%E3%83%A2%E3%83%BC%E3%83%95%E3%82%A3%E3%83%83%E3%82%AF%E9%96%A2%E9%80%A3%E4%BB%98%E3%81%91)
- [Railsのポリモーフィック関連付けを理解する](https://qiita.com/sazumy/items/726e7097c6ca4a9ca6e3)