has_many :through の関連に同一モデルを含む場合【rails4】

By | 2013年11月17日

こんな事があって困りました

売手と買手がある製品を取引するという状況を想定します。次の図のように3つのモデルを定義しました。User,Deal,Productです。ここで悩んだ点が2つありました。

  • Dealが2つのUserと関連づけられているが、どのようにして外部キーを指定するか?
  • Dealを通過してUserとProductを相互に関連付けたいがどうすればよいか?

deals


1つ目の問題に対する答えは・・・

:foreign_key => ‘外部キー名’ と :class_name => :クラス名

has_many や has_one でモデルの関連を構築する際、一般的には所有される側のモデルが、「”所有する側のモデル名”_id」という名前の外部キーをもつことになっています。ところが、このような外部キーの命名規則が不都合を生む場面はよくあります。たとえば、上図のように、売手(seller)と買手(buyer)の2つのUserが所有する Deal というモデルは、命名規則より user_id というキーを持たなくてはなりません。しかし、2ユーザーに対して同名の外部キーを持つことは出来ないため、何らかの方法で外部キーの名前を変更しなければならないという訳です。それが :foreign_key オプションを使うことで実現できます。ここでは、user_id の代わりに、seller_id と buyer_id という別名のキーでユーザーと関連付けることにします。

class User < ActiveRecord::Base
        has_many :deals, :foreign_key => 'seller_id'
end

と書けば、個々のuserインスタンスは seller_id という外部キーで管理されるようになります。
さらに、「売手としての取引」と「買手としての取引」を別々に扱うようにすることも可能です。これは :class_name オプションを使うことで実現できます。

class User < ActiveRecord::Base
        has_many :deals_of_seller, :class_name => 'Deal', :foreign_key => 'seller_id'
        has_many :deals_of_buyer, :class_name => 'Deal', :foreign_key => 'buyer_id'
end

:class_name => ‘Deal’ とすることでDealモデルを所有することができ、それと同時に、見かけ上は deals_of_seller(売手としての取引)と deals_of_buyer(買手としての取引)の2つの別名のモデルを所有しているように見せることが出来ます。一方、Deal側も同様に

class Deal < ActiveRecord::Base
        belongs_to :seller, :class_name => 'User'
        belongs_to :buyer, :class_name => 'User'
end

とすればOKです。とりあえず、これで次の図のような関係を作れたことになります。

deal3


2つ目の問題に対する答えは・・・

:through => :中間クラス名 と :source => :所有するクラス名

次に、Dealモデルを貫通してUser, Product間に相互の関連を構築する方法を考えます。一般的には、ある中間モデルを経由して2つのモデルを関連付ける場合、「:through => :中間クラス名」というオプションを使って、

class User < ActiveRecord::Base
        has_many :deals
        has_many :products, :through => :deals
end

のように書けばOKです。しかし、今回の場合は、「売手としての取引で売った製品」と「買手としての取引で買った製品」を区別するので、

class User < ActiveRecord::Base
        has_many :deals_of_seller, :class_name => 'Deal', :foreign_key => 'seller_id'
        has_many :deals_of_buyer, :class_name => 'Deal', :foreign_key => 'buyer_id'
        has_many :products_of_seller, :through => :deals_of_seller, :source => 'product'
        has_many :products_of_buyer, :through => :deals_of_buyer, :source => 'product'
end

と書きます。:source => ‘product’ と書くことで、Userモデルは deals_of_seller を貫通してProductモデルを所有することができ、それと同時に、見かけ上は products_of_seller(売った製品)を所有しているように見せることができます。deals_of_buyer 及び products_of_buyer(買った商品)も同様です。一方、Deal側とProduct側はそれぞれ、

class Deal < ActiveRecord::Base
        belongs_to :seller, :class_name => 'User'
        belongs_to :buyer, :class_name => 'User'
        belongs_to :product
end

class Product < ActiveRecord::Base
        has_many :deals
        has_many :sellers, :through => :deals
        has_many :buyers, :through => :deals
end

と書けばOKです。以上で、下の図のようなモデル関連を作ることができました。

deals

ご利益

次のようにして、それぞれのオブジェクトを取得することができます。

  • user.products_of_seller …あるユーザが売った製品
  • user.products_of_buyer …あるユーザが買った製品
  • user.deals_of_seller …あるユーザが売手として参加した取引
  • user.deals_of_buyer …あるユーザが買手として参加した取引
  • product.seller …ある製品を売ったユーザ
  • product.buyer …ある製品を買ったユーザ
  • product.deals …ある製品が売り買いされた取引

応用

FacebookやTwitterにおける友達やフォローの機能も、モデルの構造はコレとほぼ同じだと思います。