2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-02-19

管理画面でユーザーを削除しようとしたら 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
2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?