【Rails】中間テーブルの関連付けを変える際は複合インデックスに注意

Railsのマイグレーションで中間テーブルの関連付けを変更したところ、複合インデックスのトラブルが発生したため備忘録としてまとめます。

バージョン

  • Ruby 3.2.2
  • Rails 7.1.2

記事の信頼性

  • ぼくは独学で未経験から従業員300名以上の自社開発企業へ転職しました。
  • 実務ではVue.jsとRailsを毎日書いています。
  • 初心者や駆け出しエンジニアがつまづくポイントも身をもってよく理解しています。
目次

問題

中間テーブルの関連付けを変更した際に、複合インデックスがおかしくなってしまう問題がありました。

具体的には以下の状況です。

やりたいこと
– 現在、User, Post, Employee, Bookmarkテーブルが存在する
BookmarkUserPostを関連づけていたが、UserではなくEmployeePostを関連づけるよう修正したい

現時点でのdb/schema.rbの内容は次のとおりでした。

ActiveRecord::Schema[7.1].define(version: 2024_01_20_000022) do
  create_table "bookmarks", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.bigint "user_id", null: false
    t.bigint "post_id", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["post_id"], name: "index_bookmarks_on_post_id"
    t.index ["user_id", "post_id"], name: "index_bookmarks_on_user_id_and_post_id", unique: true
    t.index ["user_id"], name: "index_bookmarks_on_user_id"
  end

  create_table "employees", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.string "department", null: false
    t.string "position", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.string "title", null: false
    t.string "description", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "users", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.string "name", null: false
    t.string "email", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  add_foreign_key "bookmarks", "posts"
  add_foreign_key "bookmarks", "users"
end

Bookmarkテーブルの関連付けをUserからEmployeeに修正するため、以下のマイグレーションファイルを作成・実行しました。

# bookmarks への関連付けを user から employee へ変更
class ChangeBookmarkRelations < ActiveRecord::Migration[7.1]
  def change
    remove_reference :bookmarks, :user, index: true, foreign_key: true
    add_reference    :bookmarks, :employee, index: true, foreign_key: true
  end
end

実行後のdb/schema.rbは↓のようになります。

ActiveRecord::Schema[7.1].define(version: 2024_01_20_000939) do
  create_table "bookmarks", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.bigint "post_id", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.bigint "employee_id"
    t.index ["employee_id"], name: "index_bookmarks_on_employee_id"
    t.index ["post_id"], name: "index_bookmarks_on_post_id"
    t.index ["post_id"], name: "index_bookmarks_on_user_id_and_post_id", unique: true
  end

  create_table "employees", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.string "department", null: false
    t.string "position", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.string "title", null: false
    t.string "description", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "quiz_categories", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.string "title", null: false
    t.string "description", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "users", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.string "name", null: false
    t.string "email", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  add_foreign_key "bookmarks", "employees"
  add_foreign_key "bookmarks", "posts"
end

Bookmarkテーブルのカラムからuser_idが消えemployee_idが追加されています。

一見すると上手くいっていそうですが、よーく見ると大問題があります、、。

Bookmarkテーブルのスキーマ情報に注目してください。

# db/schema.rb からbookmarksテーブルのスキーマ情報を抜粋
  create_table "bookmarks", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.bigint "post_id", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.bigint "employee_id"
    t.index ["employee_id"], name: "index_bookmarks_on_employee_id"
    t.index ["post_id"], name: "index_bookmarks_on_post_id"
    # user_id と post_id の複合インデックスじゃなくなっている!!
    t.index ["post_id"], name: "index_bookmarks_on_user_id_and_post_id", unique: true
  end

元々はuser_idpost_idの複合インデックスでしたが、Userテーブルの関連付けを削除したことでpost_idだけが残った状態になってしまっています、、。

関連づけの修正に伴いemployee_idpost_idの複合インデックスとなって欲しかったところですが、そうはいかず、、。

複合インデックスにはユニーク制約をかける場合が多いですが、このままだとpost_idという単一のカラムにユニーク制約がかかった状態になってしまうので修正しないといけません。

解決方法

追加でインデックス修正用のマイグレーションを実行することで解決できました。

changeメソッドだとロールバックしたときに完全には元の状態に戻らないので、up/downを使っています。

# index修正用のマイグレーションファイル
class ChangeIndexOnBookmarks < ActiveRecord::Migration[7.1]
  def up
    remove_index :bookmarks, name: "index_bookmarks_on_user_id_and_post_id"
    add_index    :bookmarks, [:employee_id, :post_id], unique: true
  end

  def down
    remove_index :bookmarks, name: "index_bookmarks_on_employee_id_and_post_id"
    add_index    :bookmarks, :post_id, unique: true, name: "index_bookmarks_on_user_id_and_post_id"
  end
end

きちんとスキーマ情報が修正されました!↓

ActiveRecord::Schema[7.1].define(version: 2024_01_20_002434) do
  create_table "bookmarks", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.bigint "post_id", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.bigint "employee_id"
   # employee_id と post_id の複合インデックスになっている!!
    t.index ["employee_id", "post_id"], name: "index_bookmarks_on_employee_id_and_post_id", unique: true
    t.index ["employee_id"], name: "index_bookmarks_on_employee_id"
    t.index ["post_id"], name: "index_bookmarks_on_post_id"
  end

  create_table "employees", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.string "department", null: false
    t.string "position", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.string "title", null: false
    t.string "description", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "quiz_categories", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.string "title", null: false
    t.string "description", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "users", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.string "name", null: false
    t.string "email", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  add_foreign_key "bookmarks", "employees"
  add_foreign_key "bookmarks", "posts"
end

捕捉:そもそも最初から複合インデックスも修正するには?

今回はすでにBookmarkテーブルの関連付けを修正後に複合インデックスの不備に気づいたため、追加のマイグレーションを実行しました。

しかし、そもそも最初からインデックスも含めてBookmarkテーブルの関連付けを直していればよかった話です。

なので、その場合のマイグレーションファイルも作成してみました。

changeメソッドだとロールバックしたときに完全には元の状態に戻らないので、up/downを使っています。

# bookmarks への関連付けを変更する際に複合インデックスも合わせて修正する
class ChangeBookmarkRelations < ActiveRecord::Migration[7.1]
  def up
    remove_reference :bookmarks, :user, index: true, foreign_key: true
    remove_index     :bookmarks, name: "index_bookmarks_on_user_id_and_post_id"
    add_reference    :bookmarks, :employee, index: true, foreign_key: true
    add_index        :bookmarks, [:employee_id, :post_id], unique: true
  end

  def down
    remove_index     :bookmarks, name: "index_bookmarks_on_employee_id_and_post_id"
    remove_reference :bookmarks, :employee, index: true, foreign_key: true
    add_reference    :bookmarks, :user, index: true, foreign_key: true
    add_index        :bookmarks, [:user_id, :post_id], unique: true
  end
end

おわりに

マイグレーションはとてもややこしいですね、、。

この記事を書くにも手元でたくさん検証したんですが、無事マイグレーションが実行できたと思ったら、今度はロールバックができない、、というのを繰り返していましたね。

Railsエンジニアにおすすめの記事

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

未経験でSESから従業員300名以上の自社開発企業に転職しました。業務や個人開発で直面した問題や、転職・学習の経験を発信していきます。

コメント

コメントする

目次