📝

外部キー制約が設定されているカラムのインデックスを張り直す on Rails

2022/08/14に公開

What

Ruby on Railsを使って構築しているアプリケーションで、データベースのあるテーブルに対して "既に外部キー制約とindexが設定されているカラムにunique indexを張り直す" という作業を行おうとしたところ少し面倒だったのでその際のメモです。

Why

最近上述のアプリケーションにデータベースの制約とモデルのバリデーションを一致させるための静的解析ツールであるdatabase_consistencyを導入したところ MissingIndexChecker という怒られが発生しました。これに関する詳細な説明は公式のREADMEに記載がありますが、ざっくりいうと Foo has one Bar のような1対1のリレーションにおいてbarsテーブルのfoo_idにunique indexが張ってなかった場合に発生するものです。

元々当該カラムに外部キー制約およびindexは設定していたもののunique indexではなかったので、確かになーと思い今回設定しようとしたというのが作業の動機です。

How

最終的にマイグレーションファイルのchangeメソッドはこうなりました。

def change
  safety_assured do
    change_table :bars, bulk: true do |t|
      t.remove_foreign_key :foos
      t.remove_index :foo_id
      t.index :foo_id, unique: true
      t.foreign_key :foos
    end
  end
end

以下は上記に至るまでの流れです。

最初

add_index :bars, :foo_id, unique: true とだけ書いて実行しようとする。

def change
  add_index :bars, :foo_id, unique: true
end

既にindexがあるぞと怒られる

確かに。と思い前行に remove_index :bars, :foo_id を加える。

def change
  remove_index :bars, :foo_id
  add_index :bars, :foo_id, unique: true
end

外部キー制約があるカラムからindexは消せないぞと怒られる

確かに。と思い remove_foreign_key :bars, foosadd_foreign_key :bars, foos を処理の最初と最後に加える。

def change
  remove_foreign_key :bars, foos
  remove_index :bars, :foo_id
  add_index :bars, :foo_id, unique: true
  add_foreign_key :bars, foos
end

一応動いたようで bin/rails db:rollbackbin/rails db:migrate:redo も意図した動きになったので大丈夫そうだ。

modelにuniquenessのバリデーションがないぞと怒られる

bundle exec database_consistency を実行するとUniqueIndexCheckerが発生。

確かに。と思いBarモデルに validates :foo_id, uniqueness: true を追加。

Rails/BulkChangeTable で怒られる

rubocopを実行したところrubocop-railsの Rails/BulkChangeTable で怒られが発生。

確かに。と思い change_table :bars, bulk: true を使った書き方に修正。

def change
  change_table :bars, bulk: true do |t|
    t.remove_foreign_key :foos
    t.remove_index :foo_id
    t.index :foo_id, unique: true
    t.foreign_key :foos
  end
end

Possibly dangerous operationと怒られる

これはstrong_migrationsという別のgemの話なので本稿とは直接は関係ないが、危険な可能性のある処理だとわかって行う必要がある作業なので気をつけましょうという話。

確かに。と思いながら safety_assured のブロックで囲む。

def change
  safety_assured do
    change_table :bars, bulk: true do |t|
      t.remove_foreign_key :foos
      t.remove_index :foo_id
      t.index :foo_id, unique: true
      t.foreign_key :foos
    end
  end
end

大丈夫そう。というわけで最初のコードが出来上がり。

最後

というわけで最終的にできあがった処理は大したものではありませんでしたが、上記のように既にある程度設定済みのものを後から修正するのは面倒なので has one の関係性のテーブルを作成する場合は最初からunique indexを設定した外部キー制約をつけましょうという話でした。

余談

create_table の段階で references に外部キー制約とユニークキー制約をつけたい場合に foreign_key: true, unique: true のように書いてしまい、動かしても何故かユニークキー制約が設定されないなーとなりがち。

正しくはこう。

create_table :bars do |t|
  t.references :foo, null: false, foreign_key: true, index: { unique: true }
end
GitHubで編集を提案

Discussion