最近Rails でSTI を使う機会があったのですが、幾つかハマるポイントがありました。
1. 親クラスを継承したクラスが読み込まれない
継承した子クラスのモデルを親クラスと同じファイルにまとめていると、エラーになる場合があります。
# 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 の自動読み込みの仕組みに関連して起こるようです。
読み込みされていないクラス/モジュールがあった場合、名前から読み込みするファイルを判断できる
上記の例の場合は、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
2. 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 に変更した場合にやりがちだと思うので気をつけたいですね。