Railsのポリモーフィック関連で中間テーブルを減らす実装例(before/after)

7分で読めるテック

大学時代に金沢の観光情報サービスを個人開発していたとき、「カフェにも観光スポットにも同じタグを付けて検索できるようにしたい」という要件で設計に詰まりました。素直に実装すると中間テーブルが2本になり、タグ側からの検索も面倒になります。そこでポリモーフィック関連を使ってすっきりさせたので、その before/after を残しておきます。

解決したかった課題

Cafe(カフェ)と Spot(観光スポット)のどちらにも SearchTag(検索タグ)を紐付けたい、という要件でした。1件のカフェや観光スポットに複数のタグが付くので、多対多の関連が必要になります。

Before: 中間テーブルを2本作る方法

ポリモーフィック関連を使わない場合、モデルごとに中間テーブルが必要になります。

class Cafe < ApplicationRecord
  has_many :search_tags, through: :cafe_search_tags
  has_many :cafe_search_tags
end

class Spot < ApplicationRecord
  has_many :search_tags, through: :spot_search_tags
  has_many :spot_search_tags
end

class CafeSearchTag < ApplicationRecord
  belongs_to :cafe
  belongs_to :search_tag
end

class SpotSearchTag < ApplicationRecord
  belongs_to :spot
  belongs_to :search_tag
end

class SearchTag < ApplicationRecord
  has_many :cafe_search_tags
  has_many :spot_search_tags
end

この構成には2つの問題があります。

1つは、モデルが増えるたびに中間テーブルが1本ずつ増えていくことです。仮に HotelRestaurant を追加したくなると、HotelSearchTagRestaurantSearchTag をそれぞれ作ることになります。

もう1つは、SearchTag 側から「このタグが付いているカフェと観光スポットをまとめて取得したい」という操作が難しくなることです。cafe_search_tagsspot_search_tags を別々に問い合わせてマージする、という手間が生じます。

After: ポリモーフィック関連を使う方法

ポリモーフィック関連を使うと、中間テーブルを1本に統一できます。

class Cafe < ApplicationRecord
  has_many :search_tags, through: :destination_search_tags
  has_many :destination_search_tags, as: :destinationable
end

class Spot < ApplicationRecord
  has_many :search_tags, through: :destination_search_tags
  has_many :destination_search_tags, as: :destinationable
end

class DestinationSearchTag < ApplicationRecord
  belongs_to :destinationable, polymorphic: true
  belongs_to :search_tag
end

class SearchTag < ApplicationRecord
  has_many :destination_search_tags
end

中間テーブルは DestinationSearchTag の1本だけです。belongs_to :destinationable, polymorphic: true と宣言することで、destinationable_type(モデル名の文字列)と destinationable_id(ID)の2カラムが保存され、CafeSpot かを問わず同じテーブルで関連を管理できます。

Cafe 側と Spot 側では has_many :destination_search_tags, as: :destinationable と書くだけで、あとの扱いは通常の多対多と同じです。cafe.search_tagsspot.search_tags でそれぞれのタグを取得できます。

マイグレーションの書き方

DestinationSearchTag のマイグレーションでは、ポリモーフィック用の2カラムをまとめて宣言できます。

class CreateDestinationSearchTags < ActiveRecord::Migration[6.1]
  def change
    create_table :destination_search_tags do |t|
      t.references :destinationable, polymorphic: true, null: false
      t.references :search_tag, null: false, foreign_key: true
      t.timestamps
    end
  end
end

t.references :destinationable, polymorphic: true と書くと、destinationable_typedestinationable_id の両カラムとインデックスが自動で作成されます。

バージョン・公式リファレンス

ポリモーフィック関連付けは Rails の初期から提供されている機能で、belongs_to :xxx, polymorphic: trueas: オプションの書き方は現行の Rails でも変わりません。マイグレーションで t.references :xxx, polymorphic: true を使う記法(上記コード例で使用)は Rails 4 以降で利用できます。お使いの Rails バージョンに合わせて、下記の公式ガイドで確認してください。

参考

質問・リクエストを送る

記事についての質問や、取り上げてほしいテーマがあればお気軽にどうぞ。いただいた質問はブログ記事として回答し、Q&Aページで公開することがあります。

このサイトについて

井上 周(Amane Inoue)の個人ブログです。技術・読書・ドラマ・旅・大学生活のことを書いています。