🫣

ほんまにRailsでポリモーフィック関連付けするんか?

2024/05/13に公開2

Railsのポリモーフィック関連付け超便利ですよね!

もちろん弊社でも使ってます!

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

柔軟なデータモデリングを実現

Railsのポリモーフィック関連付けは、複数のモデルと1つの関連付けを共有できる機能です。これは、モデル間の関係をより柔軟に表現するのに役立ち、コードの冗長性を減らし、データベースの構造を簡潔に保ちます。

ユースケース

従来のRailsの関連付けでは、1つのモデルは特定の別のモデルとしか関連付けできません。しかし、ポリモーフィック関連付けを使用すると、1つのモデルは複数の異なるモデルと関連付けられるようになります。

例えば、以下のようなユースケースが考えられます。

コメント機能: ユーザーは記事、写真、動画など、さまざまな種類のコンテンツにコメントを残すことができます。
タグ付け機能: タグは記事、ユーザー、プロジェクトなど、さまざまな種類のオブジェクトに付与することができます。
いいね機能: ユーザーは記事、コメント、写真など、さまざまな種類のコンテンツにいいねすることができます。

ポリモーフィック関連付けの仕組み

ポリモーフィック関連付けは、2つの追加カラム (polymorphic_id と polymorphic_type) を使用して実装されます。

polymorphic_id: 関連付けられているオブジェクトのID
polymorphic_type: 関連付けられているオブジェクトのモデル名
これらのカラムにより、Railsは関連付けられているオブジェクトの種類とIDを特定することができます。

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

ポリモーフィック関連付けは、belongs_to と has_many アソシエーションを使用して設定できます。

Ruby
class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
end

class Article < ApplicationRecord
  has_many :comments, as: :commentable
end

class Photo < ApplicationRecord
  has_many :comments, as: :commentable
end

この例では、Comment モデルは commentable というポリモーフィック関連付けを持っています。これは、Comment が Article や Photo などの異なるモデルと関連付けられることを意味します。

Article と Photo モデルは、comments という has_many アソシエーションを持っています。as: :commentable オプションにより、これらのモデルは Comment モデルと関連付けられることを Rails に指示します。

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

ポリモーフィック関連付けには、以下のようなメリットがあります。

柔軟性: 複数のモデルと1つの関連付けを共有できるため、モデル間の関係をより柔軟に表現できます。
コードの簡潔性: 冗長なコードを削減し、コードベースをよりシンプルに保ちます。
データベースの簡潔性: 複数のテーブルを結合する必要がなくなり、データベースの構造を簡潔に保ちます。

とまあここまでがGeminiさんの説明です

長ったらしい前置きでしたが、本題はココからです。

Railsのポリモーフィック関連付けと友だちになるには

ポリモーフィック関連付けはDB設計的にはアンチパターンです。
1つのカラムに複数のモデルが紐づくだなんて、データベース大好きマンが聞いたら発狂するんじゃないかと思っています。
使用するときの注意点をまとめていきたいと思います。

検索時に工夫が必要

ポリモーフィック関連付けがされているテーブルに対して、文字列検索をするときは一工夫が必要です。

  • ポリモーフィックしてるモデルを先に検索してid取得するとか
  • 生SQLで書くとか

それでも複数のlike検索とかになるとまじでカオスです。

おのれparanoia

paranoiaは論理削除を簡単に実装してくれるgemですね。
paranoiaをモデルに設定すると、論理削除されたモデルは検索の対象外になります。
しかし、検索の対象にしたいときもあると思います。
それをポリモーフィック関連付けされたモデルに使用する場合は、以下のようにするとcommentableメソッドから分ける事ができます。

Ruby
class Comment < ApplicationRecord
  acts_as_paranoid

  belongs_to :commentable, polymorphic: true
  belongs_to :commentable_with_deleted, -> { try(:with_deleted) || unscoped }, :polymorphic => true, optional: true, foreign_key: :commentable_id, foreign_type: :commentable_type

end

class Article < ApplicationRecord
  has_many :comments, as: :commentable
end

class Photo < ApplicationRecord
  has_many :comments, as: :commentable
end

foreign_key: :commentable_idを設定すればいけるやろ!と自分は思っていましたが、foreign_type: :commentable_typeも設定する必要がありました。

公式ではこちらで使用方法が紹介されていました。

基本的にドメインの核または核に近いモデルで使うべきではない...かも?

前述のことがあるので、ドメインの核または核に近いモデルでポリモーフィック関連付けを使用するとえらいこっちゃなことになります。
枝葉になるようなそこまで重要ではないようなデータに対して使うべきかもしれません。

親→子→孫→ひ孫のひ孫あたりでポリモーフィック関連付けされても死にそうになりますが。
こうしてみるとむしろ親で使うべきな気もしてきました。
何がベストプラクティスなんだろう。

そもそもポリモーフィック関連付けで頭を抱えてしまう場面に出くわしたなら、そもそもDB設計を見直すべきかもしれませんね。
でも工数などの関係でできないこともあると思います。
そんなときは諦めましょう。

SMARTCAMP Engineer Blog

Discussion

SasakiPeterSasakiPeter

paranoiaをモデルに設定すると、論理削除されたモデルは検索の対象外になります。

この問題って、discardを採用すると解決されるらしいですね。

paranoia公式にあるように、新規プロジェクトではdiscardを採用するのが良さそうですね。

職人職人

paranoia has some surprising behaviour (like overriding ActiveRecord's delete and destroy) and is not recommended for new projects. See discard's README for more details.

本当ですね...!
私が記載した内容はparanoiaを使わざるをえないプロダクトでの解決方法ということにしておきますw