Edited at

モデルの関連・DBの定義を動的に取得する(関連が定義されていない外部キーを探す)

管理画面でユーザーを削除しようとしたら ActiveRecord::InvalidForeignKey エラーが発生!!

そうです、DBには外部キー制約が指定されているのに、モデルでは関連(has_many :emails, dependent: :destroy)を定義していなかったのです。もしかしたら、他にも関連の定義漏れがあるかもしれません。

でも、何十個もあるモデル/テーブルを1組ずつ目で見て付き合わせるのは、非効率で、信頼できません。

ここは、動的にモデル/テーブルの内容を取得して比較するスクリプトが必要です。


モデルに定義された関連を取得する

Model.reflections で取得できます。

.reflectionsはハッシュで、キーが関連名(だと思う)で、値が関連を表すオブジェクトです。関連の種類(has_many, belongs_toなど)は値のクラスで判別できます。

[16] pry(main)> User.reflections['emails']

=> #<ActiveRecord::Reflection::HasManyReflection:0x00007f8295db3e08
@active_record=
User(...),
@association_scope_cache={},
@automatic_inverse_of=nil,
@constructable=true,
@foreign_type="emails_type",
@klass=nil,
@name=:emails,
@options={:dependent=>:destroy},
@plural_name="emails",
@scope=nil,
@scope_lock=#<Thread::Mutex:0x00007f8295db3c00>,
@type=nil>


外部キーを取得する

ApplicationRecord.connection.foreign_keys(table_name) で取得できます。

[17] pry(main)> ApplicationRecord.connection.foreign_keys('emails')

=> [#<struct ActiveRecord::ConnectionAdapters::ForeignKeyDefinition
from_table="emails",
to_table="users",
options=
{:column=>"user_id", :name=>"fk_rails_328da208df", :primary_key=>"id", :on_delete=>nil, :on_update=>nil}>]

.foreign_keys は api.rubyonrails.org には載っていますが、ドキュメントはありません。


両者を比較するスクリプト

上2つのメソッドを組み合わせて、以下のようなスクリプトを作りました。なお、やっつけ仕事なので、ご自分で使うときには、例えば


  • テーブル名とモデル名が異なるので、変換が必要

  • DBが複数あるのでDBごとに比較しなければならない


  • dependent: :destroyが付いているかのチェックも必要

といった改良も必要かもしれません。

conn = ApplicationRecord.connection

db_foreign_keys =
conn.tables.map(&:to_s).flat_map do |t|
conn.foreign_keys(t).map do |fk|
[fk.from_table, fk.to_table]
end
end.sort

Rails.application.eager_load!

model_relations =
ApplicationRecord.subclasses.flat_map do |m|
m.reflections.map do |name, r|
if r.is_a?(ActiveRecord::Reflection::HasManyReflection) || r.is_a?(ActiveRecord::Reflection::HasOneReflection)
[name, m.table_name]
else
nil
end
end.compact
end.sort

pp db_foreign_keys
pp model_relations
pp db_foreign_keys - model_relations