Railsでテーブル名とクラス名が違うときのクエリの書き方の工夫について
スキルセットもニッチなら、やってる仕事もニッチな私の知見がどれほど役に立つのかわかりませんが、今週もRailsをいじっていて躓きそうになったところのTipsを書きたいと思います。
(せっかく画像用意したのに、複数形と単数形を誤ってました。。。悲しい)
Railsアプリなのに、クラス名とテーブル名が違うってどういうこと?
例えば、後々データベースの分割を行う都合から、一時的に2つのクラスが同じテーブルを指しているような状況や、既存のアプリケーションをRailsに移植するような案件の場合に当たることがあります。
この記事で議論の対象にするのは前者のケースです。
今回は、とても単純なケースとして図のようなケースを考えます。
現在稼働中のシステムは、usersテーブルに色々な情報を突っ込んでいて、これまではその全てをUserクラスで管理していました。
https://i.gyazo.com/910f5e1aa27d388a14f36e22a39bdcdc.png
今回、システムのダウンサイジングを図る目的で、UserRoleに関する責務を担うUserRoleクラスを用意して、
これまでソースコード中でUserクラスを介してUserRoleにアクセスしていたところを、徐々にUserRoleクラスを介して参照するように置き換えていくことになったわけです。
https://i.gyazo.com/842e648cf6225f0ef78b204803771c5e.png
影響を最小限に抑えるため、アーキテクトは次の流れで仕事を進めることにしました。
1. UserRoleクラスを新たに作る(このクラスは、usersテーブルのUserRoleに関する情報を扱う)
2. Userクラス経由でUserRoleを見ていた部分のソースコードをUserRoleクラスに置き換える
3. すべてUserRoleクラスに置き換えが終わったら、user_rolesテーブルを作って、UserRoleに関する情報をコピーする
4. UserRoleクラスの参照先テーブルをuser_rokesテーブルに切り替える
クエリをどう書くか?
作業を進めていくと、あるクラスがUsersへのアソシエーションを持っていることがわかりました。
code:ruby
class Hoge
belongs_to :user, optional: true
...
end
そこで、これを次のように書き換えます。
code:ruby
class Hoge
belongs_to :user_role, optional: true, foreign_key: :user_id
...
end
こうしてアソシエーションを持っているということは、このクラスが間接的にでもUsersを利用していることを示しています。
さっそく調査すると、このクラスを利用している部分で、クエリの条件として活用されていました。
code:ruby
Hoge.joins(:user).where("user.type_info = ? AND hoges.fuga >= ? AND hoges.active = 1", selected_type)
user.typeは将来的にUserRoleの持ち物になるため、単純に書き換えると
code:ruby
Hoge.joins(:user_role).where("user.type_info = ? AND hoges.fuga >= ? AND hoges.active = 1", selected_type, fuga_value)
となります。
けれども、こうして書き換えてしまうとwhereに参照先であるusersテーブルがそのまま書かれている状態になるわけです。
これでは、将来的にテーブルを分割した後、またこのモジュールに戻ってきて書き換えを行わなければならない手間が生じます。
なんとかうまく書けないものでしょうか?
scopeとmergeで解決する
UserRoleとusersテーブルを切り離すために、最終的にはself.table_nameとして参照している部分を書き換える作業をしなければなりません。
ということは、切り離しに関する作業は可能な限りUserRoleクラスの中に閉じ込めておきたいわけです。
これを鑑みて、先のコードを次のように書き換えます。
code:ruby
Hoge.joins(:user_role).where("hoges.fuga >= ? AND hoges.active = 1").merge(UserRole.type_scope(selected_type))
そして、UserRoleクラスにscopeを用意してやるわけです。
code:ruby
class UserRole
self.table_name = "users"
...
:scope type_scope, ->(user_type) {
where(type_info: user_type)
}
end
こうすることで、テーブル名を外側のクエリに出すこと無く、UserRoleクラスの中にwhere条件を閉じ込めることができました。
こうならないように設計するというのが大前提ですが、きっとこれで幸せになれる人も居ることでしょう。