やりたいこと
User毎に登録Wordを独立
させて、User毎に登録Wordのユニーク制約を適用
したい。
- 最初に作成したWordテーブルはユニーク制約を用いて同じWordの登録を不可にしている
- 新たにUserテーブルを作成した
- このままではUser全体でWordにユニーク制約がかかってしま(例:User1とUser2でそれぞれtestという単語を登録できない)
やったこと
下記の記事を参考にして、新たにマイグレーションファイルを作成しadd_index
の部分を記述した。(add_reference
は外部キー制約を追加するための記述で、ここでは説明を省く)
uniqueness: scope を使ったユニーク制約方法の解説
class AddUserToWords < ActiveRecord::Migration[6.1]
def change
add_reference :words, :user, null: false, foreign_key: true
add_index :words, %i[word user_id], unique: true
end
end
ちなみに、最初に作成したWord
のマイグレーションファイルには以下のようにユニーク制約が設けてある。
class CreateWords < ActiveRecord::Migration[6.1]
def change
create_table :words do |t|
t.string :word, null: false, limit: 100
t.index :word, unique: true
t.timestamps
end
end
end
この状態でrails db:migrate
を実行すると、schema.rb
は以下のようになる。
create_table "words", force: :cascade do |t|
t.string "word", limit: 100, null: false
t.index ["word", "user_id"], name: "index_words_on_word_and_user_id", unique: true
t.index ["word"], name: "index_words_on_word", unique: true
end
add_foreign_key "words", "users"
end
エラー内容
上記の状態でUser1
とUser2
で同じtest
という単語を登録しようとすると下記のエラーが発生した。
ActiveRecord::RecordNotUnique in WordsController#create
PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_words_on_word"
DETAIL: Key (word)=(test) already exists.
エラーの原因
エラー文に書いてあるとおり、
index_words_on_word
の部分が悪さをしているので、元々のWord
のマイグレーションファイルに記述してあるt.index :word, unique: true
が原因。
解決策
元々のWord
のマイグレーションファイルに記述してあるt.index :word, unique: true
を消去する。
マイグレーションファイルに下記を追記してrails db:migrate:reset
を実行した。
class AddUserToWords < ActiveRecord::Migration[6.1]
def change
add_reference :words, :user, null: false, foreign_key: true
add_index :words, %i[word user_id], unique: true
remove_index :words, :word
end
end
無事にUser毎に登録Wordを独立
させて、User毎に登録Wordのユニーク制約を適用
することができた。
学んだこと
これまでデータベース周りをいじることが少なかったため、理解すると簡単なことだったが解決までに時間を要してしまった。
今回の経験を通じてデータベース周りの理解が深まった。
補足
今回はデータベースをリセットしても問題ないため、rails db:migrate:reset
を実行した。
リセットしたら困る場合は新たにマイグレーションファイルを作成し、そこにremove_index
を記述し、rails db:migrate
で対応する必要があると考える。
また、リセットして構わないのであれば、最初のWord
マイグレーションファイルからt.index :word, unique: true
を削除するのでも良かったかもしれない。
ちなみにモデル側でも下記のようにscopeを指定してユニーク制約を設けている。
class Word < ApplicationRecord
belongs_to :user
validates :word, presence: true, uniqueness: { scope: :user_id }, length: { maximum: 100 }
end
データベースとモデルの両方でユニーク制約をつける意味については、Railsチュートリアルの説明が理解しやすかったです。