Edited at

[Rails] [RDB] キー・複合キーをきちんと利用する

More than 1 year has passed since last update.


環境

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作った段階で必ずはるようにすると良いと思う。