やりたいこと
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チュートリアルの説明が理解しやすかったです。