📔

【Rails】複合ユニークインデックスを変更したい時の手順

2024/03/26に公開

こんにちは!
ラブグラフエンジニアのひろです。

今回は、複合ユニークインデックスを変更する際に
Mysql2::Error: Cannot drop index 'index_name': needed in a foreign key constraint
というエラーに直面した時の解決策について書きます。

例として、 profiles テーブルの user_idhandle_name による複合ユニークインデックスを削除し、 user_idemail による複合ユニークインデックスを作成するプロセスを用いて解説します。

前提条件

  • profiles テーブルには user_idhandle_name カラムが存在する
  • users テーブルとの外部キー制約が貼られている
  • user_idhandle_name の組み合わせで複合ユニークインデックスが設定されている
  • user_idemail を使った複合ユニークインデックスに変更したい
この時点での db/schema.rb
create_table "users", force: :cascade do |t|
  t.string "name"
  # 他のユーザー属性...
  t.timestamps
end

create_table "profiles", force: :cascade do |t|
  t.bigint "user_id", null: false
  t.string "handle_name"
  t.string "email"
  # 他のプロフィール属性...
  t.timestamps
end

add_index "profiles", ["user_id", "handle_name"], unique: true, name: "index_profiles_on_user_id_and_handle_name"

add_foreign_key "profiles", "users", column: "user_id"

発生した事象

新しい複合ユニークインデックスを作りたかったので、既存のものを削除してから新規で作成する migration ファイルを用意しました。

class ChangeUniqueIndexToUserIdAndEmailOnProfiles < ActiveRecord::Migration[7.0]
  def up
    remove_index :profiles, name: 'index_profiles_on_user_id_and_handle_name'

    add_index :profiles, [:user_id, :email], unique: true, name: 'index_profiles_on_user_id_and_email'
  end

  def down
    remove_index :profiles, name: 'index_profiles_on_user_id_and_email'

    add_index :profiles, [:user_id, :handle_name], unique: true, name: 'index_profiles_on_user_id_and_handle_name'
  end
end

しかし、そのままインデックスを更新しようとすると

Mysql2::Error: Cannot drop index 'index_profiles_on_user_id_and_handle_name': needed in a foreign key constraint

というエラーが発生します。

原因の解析

Mysql2::Error: Cannot drop index 'index_profiles_on_user_id_and_handle_name': needed in a foreign key constraint

Cannot drop index とあるので削除に失敗しているようです。

原因はなんでしょうか?

needed in a foreign key constraint なので 外部キー制約によって必要とされていることも分かりますね。

インデックスが削除できない問題の根本原因は、外部キー制約によってインデックスが必要とされていることにあります。
そのため、インデックスを削除する前に、関連する外部キー制約を削除または変更する必要があることが分かりました。

解決策

問題の解決には、以下の手順を踏みます。

  1. 外部キー制約の削除
  2. インデックスの削除
  3. インデックスの再作成
  4. 外部キー制約の再作成

解決手順

1. 既存の外部キー制約の削除

最初に外部キー制約を削除します。

class RemoveForeignKeyFromProfile < ActiveRecord::Migration[7.0]
  def change
    remove_foreign_key :profiles, :users
  end
end

2. インデックスの削除

外部キー制約が無くなったことで、既存の複合ユニークインデックスを削除できるようになっています。
以下のようにマイグレーションファイルを作成し、削除します。

class RemoveUniqueIndexFromProfile < ActiveRecord::Migration[7.0]
  def change
    remove_index :profiles, name: :index_profiles_on_user_id_and_handle_name
  end
end

3. インデックスの再作成

最後に、新しい組み合わせで複合ユニークインデックスを再作成します。
再びマイグレーションファイルを作成し、以下のコードを追加します。

class AddNewUniqueIndexToProfile < ActiveRecord::Migration[7.0]
  def change
    add_index :profiles, [:user_id, :email], unique: true
  end
end

このマイグレーションにより、user_id と email カラムに基づいて新しい複合ユニークインデックスが作成されます。

4. 外部キー制約の再作成

ステップ1で削除した外部キー制約を復活させます。

class AddForeignKeyToProfiles < ActiveRecord::Migration[7.0]
  def change
    add_foreign_key :profiles, :users, column: :user_id
  end
end

解決後

最終的に schema.rb はこのようになりました。

db/schema.rb
create_table "users", force: :cascade do |t|
  t.string "name"
  # 他のユーザー属性...
  t.timestamps
end

create_table "profiles", force: :cascade do |t|
  t.bigint "user_id"
  t.string "handle_name"
  t.string "email"
  # 他のプロフィール属性...
  t.timestamps
end

add_index "profiles", ["user_id", "email"], unique: true, name: "index_profiles_on_user_id_and_email"

add_foreign_key "profiles", "users", column: "user_id"

まとめ

この記事では、複合ユニークインデックスを変更する方法について解説しました。
紹介したステップを適切に実行することで、エラーを避けながらデータベースのスキーマ変更をおこなうことができます。

この記事が同じ問題に直面した方の助けになれば幸いです。

ラブグラフのエンジニアブログ

Discussion