最近Rails でSTI を使う機会があったのですが、幾つかハマるポイントがありました。
親クラスを継承したクラスが読み込まれない
継承した子クラスのモデルを親クラスと同じファイルにまとめていると、エラーになる場合があります。
# app/models/user.rb class User < ActiveRecord::Base end class Admin < User end class Guest < User end # rails console で実行 irb(main):001:0> Admin.all NameError: uninitialized constant Admin
この問題はRails の自動読み込みの仕組みに関連して起こるようです。
Rails の自動読み込みの話
読み込みされていないクラス/モジュールがあった場合、名前から読み込みするファイルを判断できる
上記の例の場合は、admin.rb を読み込もうとしてエラーになります。
つまり、Adminクラスにアクセスする前にUserクラスが読み込まれていれば、NameError: uninitialized constant エラーは発生しません。
Devise等を利用していて、ログイン処理をUserクラスでまとめていたりすると before_action で毎回User にアクセスしているため
エラーが起こらないということもあると思います。
また、production モードでも起動時にmodel が読み込まれるため発生しません。
以上のような理由で場合によっては1つのファイルにまとめて記述しても問題ないかもしれませんが、大抵の場合は子クラスは別ファイルに分けたほうがいいと思います。
# app/models/user.rb class User < ActiveRecord::Base end
# app/models/admin.rb class Admin < User end
# app/models/guest.rb class Guest < User end
Routing がおかしくなる
子クラスのcontroller を作成せず親クラスのcontroller で処理をまとめていると
create update などのaction からのリダイレクト先が 子クラスのURLになってしまってエラーになる場合があります。
# config/routes.rb Sample::Application.routes.draw do root 'users#index' resources :users end # app/controllers/users_controller.rb def create @user = if admin? Admin.new(user_params) elsif guest? Guest.new(user_params) else raise end if @user.save redirect_to @user else render action: :new end end # undefined method `admin_url' for #<UsersController:0x007af349879fa8>
controller を一つにまとめたい場合は becomes を使うと上手く行くと思います。
This is mostly useful in relation to single-table inheritance structures where you want a subclass to appear as the superclass.
目的にピッタリ合いそうです。
# app/controllers/users_controller.rb def create # 省略 if @user.save @user = @user.becomes(User) #この行を追加 redirect_to @user else render action: :new end end
これで無事 UsersController#show が表示されました。
まとめ
2つ目のRouting に関する問題は、後からSTI に変更した場合にやりがちだと思うので気をつけたいですね。