特定のクラスを参照しているActiveRecord関連を全て列挙する
Photo by 愚木混株 cdd20 on Unsplash
DBモデルのビジネスロジックを変更するにあたって、影響範囲の調査のために関連を列挙したいことがあります。ActiveRecordの場合、あるクラスから出ている関連は以下の方法で簡単に列挙できます。
User.reflect_on_all_associations
一方、あるクラスを参照している関連を列挙するのはそれほど簡単ではなさそうでした。inverse_ofは取れるものの、全ての関連に逆関連があるわけではありません。
結局網羅的な検索のため、以下のようなコードを書くことになりました。
pp ActiveRecord::Base.descendants
.select { |k| !k.abstract_class? }
.flat_map { |k| k.reflect_on_all_associations }
.uniq
.filter { |r| r.is_a?(ActiveRecord::Reflection::AssociationReflection) && !r.polymorphic? }
.reject { |r| [:some_bad_association].include?(r.name) }
.filter { |r| r.klass == User }
.map { |r| "#{r.active_record}##{r.name}" }
以下、このコードを解説します。
以下のコードで、全ての既知のActiveRecordクラスを列挙しています。abstract class (ActiveRecord上でabstractというフラグのついたクラス) には興味がないので外しています。
ここで観測したい全てのクラスが入っている必要があるため、先に全てのクラスをeager loadしておくことが重要です。これはRailsのオプションで調整できるため、ここでは行っていません。
ActiveRecord::Base.descendants
.select { |k| !k.abstract_class? }
以下のコードで、全てのクラスの全ての関連を抽出しています。STIなどのためにスーパークラスで関連を宣言する場合は全てのサブクラスで同じassociationが返ってくるので、uniqを取っています。
.flat_map { |k| k.reflect_on_all_associations }
.uniq
以下のコードで、has_one/has_many/belongs_to関連のみを抽出しています。through関連は単なるビューであるためここでは外しています。また、polymorphic関連の参照先クラスはスキーマだけではわからないため、後続の処理でのエラーを回避するために除外しています。polymorphic関連は別途手動で調査することをおすすめします。
.filter { |r| r.is_a?(ActiveRecord::Reflection::AssociationReflection) && !r.polymorphic? }
続いて以下のコードで既知の問題ある関連を除外します。リファクタリングやコード整理によって関連が使われなくなり、関連の参照先クラスが無くなっても、コードは正常に動作し続けます。しかし、ここで参照先クラスを調べようとするとエラーになってしまうので、そういった関連は手動で除外する必要があります。ここで除外した関連はあとで消してあげましょう。
NameErrorをrescueしてもうまく動作せず正常系に復帰しなかったため、ここではrescueしていません。
.reject { |r| [:some_bad_association].include?(r.name) }
これで参照先クラスを r.klass
で計算できるようになったので、所望の関連のみを取り出します。そして、読みやすい形で表示します。
.filter { |r| r.klass == User }
.map { |r| "#{r.active_record}##{r.name}" }