STI で type 列に名前空間を除いたクラス名のみを格納する [Ruby on Rails]
結論
store_full_sti_class というクラス変数があるので、スーパークラスで false
をセットする。
class Chat::Room < ApplicationRecord
self.store_full_sti_class = false
end
解説
ユースケース
モノリシックな大規模アプリケーションになってくると、モデルクラスも名前空間で整理したくなることがあります。例えば、チャットのサブシステムを作るにあたってテーブル名は chat_rooms
, chat_members
, chat_messages
といった形で用意したいのですが、デフォルトでは app/models/chat_room.rb, app/models/chat_members, app/models/chat_messages とトップレベルに chat_
というプレフィックスが付いたファイルが大量にあると管理が辛いので app/models/chat/room.rb, app/models/chat/member.rb, app/models/chat/message.rb のように chat ディレクトリにまとめてしまい、クラスも ChatRoom
, ChatMember
, ChatMessage
とするのではなく Chat::Room
, Chat::Member
, Chat::Message
としておけば、Chat
という名前空間内では「ルームと言えば当然チャットルームだよね」という世界観でいられるのでプログラミングが楽になります。
例えば、ここでチャットルームにはプライベートルームとパブリックルームとお知らせ専用の一方通行ルームの3つの種別があって、テーブルやデータ構造は同じだけど少しだけ種別毎にインスタンスメソッド等を用意したいため STI を使って chat_rooms
テーブルの type
列にプライベートルームかパブリックルームかお知らせルームかをクラス名で格納することにしたとします。モデルクラスとしては Chat::PrivateRoom
と Chat::PublicRoom
と Chat::NoticeRoom
のように作ってそれぞれ Chat::Room
を継承することになるのですが、このまま Ruby on Rails のデフォルト状態で STI しようとすると type
列には Chat::PrivateRoom
のように名前空間まで文字列で保存されてしまい、chat_rooms
という名前のテーブルの属性に Chat::
という名前空間まで格納するのは少し気持ち悪いので取り除けないか、というのが本題です。
本体の実装を読み解く
「rails sti 名前空間」とか「rails sti without namespace」とか調べてみても誰も同じような悩みを抱えていないようなので、本体の実装を見に行くことにしました。STI 関連の実装は activerecord/lib/active_record/inheritance.rb にあります。
sti_name
さらっと読んでみて目を引くのは sti_name
というクラスメソッドです。
# Returns the value to be stored in the inheritance column for STI.
def sti_name
store_full_sti_class && store_full_class_name ? name : name.demodulize
end
このクラスメソッドが返す値が type
列に保存される文字列らしいので、これをオーバーライドしてやれば任意の文字列で type
列を管理することができます。
class Chat::PrivateRoom < Chat::Room
def self.sti_name
'PrivateRoom'
end
end
また、名前空間付きの文字列("Chat::PrivateRoom"
)は name
で取得でき、これを String#demodulize
で名前空間を除いた文字列を取得することができます。あるいは ActiveRecord::Base.class_name
で取得することもできます。name.demodulize
は ActiveRecord::Base.name
はただのクラス変数であるのに対して、class_name
はメソッドで table_name
を元にクラス名を作り出す処理なので、いろんな意味で前者の方が良いかもしれません。これでスーパークラスに1つ書けば良いことになりました。
class Chat::Room < ApplicationRecord
def self.sti_name
name.demodulize
end
end
store_full_class_name
任意の文字列を格納したい場合は sti_name
をオーバーライドすれば良いわけですが、今回の目的はあくまで名前空間を取り除くことだけです。冒頭に結論を書いているのに、わざわざここまで読み進めている暇なお前らには既にピンと来ていたかもしれませんが、先ほどの sti_name
の実装にそのまま答えが書いてあります。もう一度見てみましょう。
# Returns the value to be stored in the inheritance column for STI.
def sti_name
store_full_sti_class && store_full_class_name ? name : name.demodulize
end
このメソッドは store_full_sti_class
と store_full_class_name
が共に真の場合には name
(名前空間付き文字列)を、そうでない場合は name.demodulize
(名前空間を除いた文字列)を返しています。つまり、store_full_sti_class
と store_full_class_name
のどちらかが偽であれば name.demodulize
を返してくれるわけで、名前からすると store_full_sti_class
を偽にするのが良さそうです。store_full_sti_class
も同じく activerecord/lib/active_record/inheritance.rb に実装されています。これはただの class_attribute
で作られたクラス変数で、デフォルトは true
がセットされています。
# Determines whether to store the full constant name including namespace when using STI.
# This is true, by default.
class_attribute :store_full_sti_class, instance_writer: false, default: true
つまり、このクラス変数を false
にしてやるだけで今回の課題は解決できるので sti_name
をオーバーライドする必要はありません。
class Chat::Room < ApplicationRecord
self.store_full_sti_class = false
end
> Chat::NoticeRoom.new.type
=> "NoticeRoom"
以上です。
Discussion