環境
Rails 5.0.0.1, MySQL 5.6
概要
Railsでは主キーは基本的にidをサロゲートキーとして用いる。
そのことの良し悪しはここでは論じないが、自然キーや複合キーがあるのであれば、それを意識し、正しく扱うことが重要であることは変わらない。
といっても基本的にはunique indexはるだけではある、それについてのメモ。
本編
ユーザが本質的に1日1回しかできない投稿がある、という例で考える。
users と daily_posts というテーブルがある。
usersのテーブル定義は省略し、daily_postsのmigrationファイルを正しく作る。
まず複合キーのことを考えずに、daily_postsのmigrationを作るとこんな感じになる。(作っただけでまだ db:migrate
していない状態。)
def change
create_table :daily_posts do |t|
t.references :user, foreign_key: true, null: false
t.date :date, null: false
t.string :title, null: false
t.timestamps
end
end
1. キーを認識する
「本質的にuserが1日1回しかpostできない」ので、user_id + date は複合キーである。
これに対して、忘れずにunique indexをはるのが重要である。
注意: indexは、リレーショナルモデルに直接関係ない。一方、キーや複合キーはリレーショナルモデル上の重要な概念であり、それに「unique制約をつける」のが重要である。
unique制約のために、実質的にindexが必要であるから、unique indexをはる。
(ここまで書いて思ったが、unique制約は a + b
だけどindexは a -> b -> c
ではりたいときに、railsでどうできるのかは知らない、、)
2. (複合キーの場合) unique indexの順序を決める
理論的には user_id -> date
でも date -> user_id
でもどちらでも良い。使えそうな順序にする。
今回は user_id -> date
にする。
3. キーに対してunique indexをはる
migrationに、 add_index :table_name, [:key1, :key2, ...], unique: true
と書くことで実現できる。
def change
create_table :daily_posts do |t|
t.references :user, foreign_key: true, null: false
t.date :date, null: false
t.string :title, null: false
t.timestamps
end
add_index :daily_posts, [:user_id, :date], unique: true
end
4. 無駄なindexが残っていないか注意する
複合のunique indexをはると、元あったindexが無駄になることがある。
上の例では、
-
t.references
は、外部キーをはりながらindexをはってくれる機能。 -
t.references :user
により、user_id
に対しindexがはられている。 - 今
user_id -> date
のindexをはった。したがってuser_id
だけのindexは無駄である。
よって、 t.references
をやめて、単に外部キーをはるだけにする。
すなわち、普通にカラムを定義したあとに add_foreign_key
をするように変更する。
def change
create_table :daily_posts do |t|
t.integer :user_id, null: false
t.date :date, null: false
t.string :title, null: false
t.timestamps
end
add_index :daily_posts, [:user_id, :date], unique: true
add_foreign_key :daily_posts, :users
end
追記: 以下のように、t.referencesにindex:falseのオプションを追加しても問題ありません。こちらのほうが簡単だったかもしれません。
def change
create_table :daily_posts do |t|
t.references :user, foreign_key: true, index: false, null: false
t.date :date, null: false
t.string :title, null: false
t.timestamps
end
add_index :daily_posts, [:user_id, :date], unique: true
end
まとめ
テーブル設計=>migration作る=>unique indexはる=>不要なindexが生成されないか注意。
unique制約はテーブル設計のうちなので、migration作った段階で必ずはるようにすると良いと思う。