HABTMでforeign_keyとかclass_nameを駆使してみる

最近インターン生に,「まだできないのー?(ニヤニヤ」ってやりながら,Rails3.0のプロジェクトをRails4.1.1まで一気に引き上げるというお仕事をしています.
こんばんは,@h3_potetoです.

ブログもインターン生二人に書いてもらうことが増えると思いますので,暖かく見守ってあげてください.



さて,前の記事で@shunkurosakiくんにRailsの,モデルのAssociationについて書いてもらいました.
Active Record Associationsは,非常に便利なので,使いどころを身につけておくと強くなれます.

そこで,今回はhas_manyやbelongs_toを,ちょっと突っ込んだ使い方してみましょう.

HABTMというのは,CakePHPRailsでよく使う,モデル間のリレーションです.
その辺は,こんなサイトやら
http://www.stonedot.com/lecture6.html

こんなサイト
http://www38.atwiki.jp/eyes_33/pages/45.html

を参照してくれた方がわかりやすいと思います.


例えば,Userというモデルに,ユーザを登録しておきます.
そのユーザが複数の言語を話せるとしましょう.

言語のモデルをLanguageとして作っておきます.
初期状態では,二つのモデルは以下のような定義になっていると思います.


app/models/user.rb

class User < ActiveRecord::Base
end


app/models/language.rb

class Language < ActiveRecord::Base
end

DBとしては,usersのテーブルが

id name
1 @h3_poteto

こんな感じ.
languagesのテーブルが

id language
1 日本語
2 英語

となっているとしましょう.


さぁ,これらを関連づけたいのですが,一人のユーザが複数の言語を話せるという状況は往々にしてあります.
ということは,Userは複数のLanguageを参照できなければなりません.

そのために中間テーブルを作ります.
DBの構成としては,

id user_id language_id
1 1 1
2 1 2

です.
モデルは,

app/models/user_language.rb

class UserLanguage < ActiveRecord::Base
end

みたいなものが生成できていればよろしい.



さて,これをActiveRecordを介した状態で,上手いこと扱えるようにモデルの定義を書きます.


app/models/user.rb

class User < ActiveRecord::Base
  has_many :user_languages
  has_many :languages, :through => :user_languages
end


app/models/user_language.rb

class UserLanguage < ActiveRecord::Base
  belongs_to :language
end


と,これで,

user = User.find(1)
user.languages

みたいな参照の仕方ができるわけですね.




ここまでが,前回@shunkurosakiくんが書いてくれた内容のおさらいです.

ここから,逆側の参照も作ってみる

あれ?日本語が話せるユーザって誰だっけ?と考えたとします.

そうすると,日本語が話せるユーザも複数人いますよね,絶対.
だから,LanguageからUserへも,has_manyしてやります.


app/models/language.rb

class Language < ActiveRecord::Base
  has_many :user_languages
  has_many :users, :through => :user_languages
end


app/models/user_language.rb

class Language < ActiveRecord::Base
  belongs_to :language
  belongs_to :user
end

と,書き足してやると,

language = Language.find(1)
language.users

というように参照してやれますね.



中間テーブルのカラム名がモデル名と違うケース

そして,それなりに開発していると,実はDBのテーブル構成として,user_idとかlanguage_idっていう,素直にモデル名+idというカラムを作れない状況が出てくるんですね.
そうした場合でも,このモデルの定義を改変していくことで,同じように参照してやることができます.

そのために使うのが,:class_name:foreign_key


例えばですね,DBの中間テーブル,user_languagesを,実はテーブル名も変えて構成も以下のように変えてみました.


people_speakingsテーブル

id people_id speaking_id
1 1 1
2 1 2

前述と同じなのですが,カラム名だけが違います.

user_id => people_id
language_id => speaking_id

と変更してみました.


これで,先ほどと同様に参照できるように,モデルの定義だけを書き換えて行きます.
注意:user_languagesテーブルをpeople_speakingsテーブルに変更したため,モデル名もuser_lanuage.rbからpeople_speaking.rbに変更になりました.


app/models/user.rb

class User < ActiveRecord::Base
  has_many :user_languages, :class_name => "PeopleSpeaking", :foreign_key => :people_id
  has_many :languages, :through => :user_languages
end

app/models/language.rb

class Language < ActiveRecord::Base
  has_many :user_languages, :class_name => "PeopleSpeaking", :foreign_key => :speaking_id
  has_many :users, :through => :user_languages
end


app/models/people_speaking.rb(旧:user_language.rb)

class PeopleSpeaking < ActiveRecord::Base
  belongs_to :user, :class_name => "User", :foreign_key => :people_id
  belongs_to :laguage. :class_name => "Language", :foreign_key => :speaking_id
end

解説

app/models/user.rbの

has_many :user_languages, :class_name => "PeopleSpeaking", :foreign_key => :people_id

これは,まず中間テーブルをhas_manyさせます.
ただし,詳しく指定をしています.

(class_nameで指定された)PeopleSpeakingモデルの,(foreign_keyで指定された)people_idカラムの値が,自信のモデルのprimary_key,つまりusersテーブルのidと一致するレコードを引っ張ってきて,:user_languagesとしてhas_manyします.

そして,次の行

has_many :users, :through => :user_languages

で,先ほどhas_manyした:user_languagesの,該当するレコードの持つ:userを複数個持てるように指定してあります.

:user_languagesの持つuserとは?

app/models/people_speaking/rb

belongs_to :user, :class_name => "User", :foreign_key => :people_id

ここで,指定されている:userのことです.

この行では,PeopleSpeaking自身が持っている(foreign_keyで指定された):people_idカラムの値が,(class_nameで指定された)Userモデルのprimary_key,つまりusersテーブルのidと一致するusersテーブルのレコードを,PeopleSpeakingモデルの:userとして参照可能にしています.




つまり,テーブル名とhas_manyさせるときに使うカラム名が違う場合,

・foreign_keyは中間テーブルの該当カラム名
・class_nameはどのテーブルのレコードを引っ張りたいのか
・has_manyするときの名前は,belongs_toで指定した名前の複数

と指定していくと,整合性がとれて行きます.




冒頭で書いたHABTMでは,テーブル名とカラム名をきっちりそろえているので,その辺の指定をRails側で解決できるのですね.



以上,ちょっと難しくモデル間のAssociationについて書いてみました.

あとは,モデルの定義を書きながら,rails consoleで参照してみると良いと思います.
定義が間違っていれば参照できずにエラーが出ると思いますので,そうやって試行錯誤しているとだんだん身に付いてきます.