Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

管理画面でユーザーを削除しようとしたら 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
tonluqclml
エムスリーでソフトウェアエンジニアしています。仕事ではRubyもScalaもPythonもBashもなんでもやる雑食系。 Twitter:https://twitter.com/doloopwhile 昔の個人ブログ:http://doloopwhile.hatenablog.com/ 勤務先ブログ: https://www.m3tech.blog/
http://doloopwhile.hatenablog.com/
m3dev
インターネット、最新IT技術を活用し日本・世界の医療を改善することを目指します
https://m3.recruitment.jp/engineer/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away