同一モデル間の中間テーブルを作成する
同一モデルを含む中間テーブルを作成して、そこで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
ええ感じ!