プログラミング 美徳の不幸

Ruby, Rails, JavaScriptなどのプログラミングまとめ、解説、備忘録。

Railsの単一テーブル継承モデルの子モデルにおけるform_for

ひさびさにRailsでハマりました。(この記事は過去にも書いたんですが誤りがあったので全面的に新しくしたものをうpします)

◯やりたいこと
・User name:string type:string
・Teacher (Userを継承している)
・Student(Userを継承している)
・Item name:string

構成は
user =has_many=>items
で、なおかつTeacherとStudentがusersテーブルを単一テーブル継承。

そしてなおかつresourceはネストされる
◯config/routes.rb

resources :user do
  resources :item 
end

これであとはごく通常どおりのCRUDが出来ればいいんですが、普通は詰まるんですね、これ。

というのはitemのform_forで、通常のネストされたresourcesの場合は引数に配列を渡します。

form_for[@item.user,@item] do
 //
end

ちなみにこの場合でも@item.userがnilのとき(つまりusers/2/items/newなどのとき)のことを考えないとならないので実際は

form_for[@item.user || :user,@item] do
//
end

のように書くことになると思います。

ですが、今回のように@item.userが実際はTeacherかStudentのモデルが返される場合はどうなるかというと
・newのときはundefined method `items_path'
・editのときはundefined method `teacher_item_path' (あるいは'student_item_path')
というエラーが発生します。

前者は結局@item.userがnilなので、配列が後ろだけ評価されるということなのでしょう。まぁ最悪@item.user || :userと書けばこれは防げます。
問題は後者です。これは@item.userがUserモデルではなくTeacherあるいはStudentの、単一テーブル継承されたモデルのほうを返すためにこういうことになるのです。

結論から言って、これを綺麗に書くことはたぶん不可能です。とはいえ、form_forの記述部分を_formからedit.html.erb,new.html.erbに移し替える、なんてのは恥ずかしい。

従って僕はこうしました。

◯app/controllers/items_controller.rb

(略)
def new 
  @item = Item.new
  @item.user = User.find(params[:user_id])
  (略)
end

def edit
  @item = Item.find(params[:id])
end

◯app/views/items/_form.html.erb

form_for(@item.user.becomes(User),@item) doend

becomesというのは単一テーブル継承においてモデル名を明示的に変更するためのメソッド、のようです。

重要なのはnewのときに@item.user = User.find...で突っ込んじゃうこと。これは
def new
@item = User.find(params[:user_id]).items.new
end
と書いても同じなんですが、コードの見た目的に僕は2行に分けて書いてもいいかなと思います。

なぜここで突っ込まないといけないかというと、そうしないとnewのときに
@item.user.becomes(User)が例外を吐くためです。(@item.userがnilになる、NoMethodError)



いかがでしょうか。ちなみにStackOverFlowで同じ問題にぶち当たった人の回答の中にはitem.userのモデル名をオーバーライドするなんてものもありましたが、副作用がありそうなのでオススメできません。

これが一番綺麗なんじゃないかな!?