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) do 略 end
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のモデル名をオーバーライドするなんてものもありましたが、副作用がありそうなのでオススメできません。
これが一番綺麗なんじゃないかな!?