同一モデル間の中間テーブルを作成する

同一モデルを含む中間テーブルを作成して、そこでhas_oneやhas_manyしたい状況がありました。

以下がとても参考になりました。

has_many :through の関連に同一モデルを含む場合【rails4】 | Coma's Tech Blog

HABTMでforeign_keyとかclass_nameを駆使してみる - インターファーム開発部ブログ

こんな感じ

Uberのようなプロモーションコードを使った履歴を管理する機能を想定します。

中間テーブルで用いるフィールドの名前は、 user_id などのようにできないので、それぞれの役割に応じて理解しやすい名前をつけます。

Userモデルが、片方がinviter、もう片方がinvited_userとして PromotionCodeHistoryを持ちます。関係は、以下のようにします。

  • inviter has_one promotion_code_history
  • invited_user has_many promotion_code_histories

PromotionCodeHistoryのmigrationファイルは以下のような感じ。

# db/migrate/20160115080633_create_promotion_code_histories.rb
class CreatePromotionCodeHistories < ActiveRecord::Migration
  def change
    create_table :promotion_code_histories do |t|
      t.integer :inviter_id, index: true, foreign_key: true
      t.integer :invited_user_id, index: true, foreign_key: true

      t.timestamps null: false
    end
  end
end

このとき、Userモデルでは以下のように擬似的なフィールド名を渡して、associationの対象のclass名とkeyの名前を明示してやります。

class User < ActiveRecord::Base
  has_one  :inviter_of_promotion_code_history,
           class_name: "PromotionCodeHistory",
           foreign_key: "inviter_id"
  has_many :invited_user_of_promotion_code_history,
           class_name: "PromotionCodeHistory",
           foreign_key: "invited_user_id"
  # ...
end

そしてPromotionCodeHistoryモデルでは、以下のように擬似的なモデル名を渡した後に、それが本当はどのclassを指してるのか明示するために、class名を指定してやります。

class PromotionCodeHistory < ActiveRecord::Base
  belongs_to :inviter, class_name: "User"
  belongs_to :invited_user, class_name: "User"

  validates :inviter_id, presence: true
  validates :invited_user_id, presence: true
end

これでいけます!わーい

テスト

テストは、直感通りに書けばよいみたいです。

Userモデルの単体テスト

# spec/models/user_spec.rb
RSpec.describe User, type: :model do
  describe "association" do
    it { is_expected.to have_one :inviter_of_promotion_code_history }
    it { is_expected.to have_many :invited_user_of_promotion_code_history }
  end

続いてPromotionCodeHistoryモデルの単体テスト

# spec/models/promotion_code_history.rb
RSpec.describe PromotionCodeHistory, type: :model do
  describe "association" do
    it { is_expected.to belong_to :inviter }
    it { is_expected.to belong_to :invited_user }
  end

  describe "validation" do
    it { is_expected.to validate_presence_of :inviter_id }
    it { is_expected.to validate_presence_of :invited_user_id }
  end
end

ええ感じ!