Railsでreferencesを使用した外部キーに、同時にUnique属性を設定する
Railsで新しいモデル(テーブル)の作成で外部キー(外部参照)を設定する時に、
同時にユニーク属性を設定していく手順を記載します。
余り使う機会はないかもしれませんが、今回は以下の例を使っていきます。
(例)Userが複数のOrganizationに所属できる多対多の関係で、そのうちの一のOrganizationをデフォルトとして保持するDefaultOrganizationテーブルを作成する場合。
以下がER図です。
試行錯誤して結果を得たので、経過も踏まえて記述します。 解答に書いている事がやり方なので、手早く正解が欲しい方はそちらをご覧下さい。
なぜ必要なのか
使用しているDBにもよりますが、先に出した例であれば、中間テーブルにデフォルトフラグ的な項目があれば良いと思います。
しかし、それだとDefaultOrganizationはユーザーにつき1つだけというユニーク属性(一意制約)を設定できない場合があります。
アプリ側で制限したら良いよってポリシーの場合は特段ここまで考える必要はありませんが、
DBのリレーションまでキチンと設計しておきたい場合、PostgreSQLやMySQLであれば部分インデックスで可能ですが、OracleやMySQLだと部分インデックスが使えないって制限があります。
そういった場合に使えるかと思います。
なお、中間テーブルと、中間テーブルのフラグについては、こちらに書きました。
普通に外部参照だけをする場合
マイグレーションファイルを作成します。
rails g migration CreateDefaultOrganization user:references relation:references
※この時に、userとorganizationにidを付与しません。
referencesにした場合、勝手に補完されますので、モデル名だけで良いです。
すると、以下の様なマイグレーションファイルが作成されます。
class DefaultOrganization < ActiveRecord::Migration def change create_table :default_organizations do |t| t.references :user, index: true, foreign_key: true t.references :relation, index: true, foreign_key: true t.timestamps null: false end end end
これをrake db:migrate
すれば中間テーブルの完成です。
非常に簡単ですね。railsの素早さを実感できます。
この場合、DBを見て頂くと分かりますが、
foreign_keyは有効になっており、インデックスも作成されています。
index作成と同じ要領でuniqueしてみる
そこで、indexを作成する要領でunique属性を付けてみます。
1ファイル内で指定
class DefaultOrganization < ActiveRecord::Migration def change create_table :default_organizations do |t| t.references :user, index: true, foreign_key: true, unique: true t.references :relation, index: true, foreign_key: true t.timestamps null: false end end end
エラーは出ませんが、前回と同じ設定となってユニークは設定されません。
後にわかりますが、ここでforeign_keyとuniqueの同時指定をする場合はこの記述方法だと効果がありません。
2ファイル内で別々に指定
同時していするのがダメなのかと思い、一端default_organizationsテーブルを作成し、
そこにインデックスを付与して、外部参照キーを設定してみます。
class DefaultOrganization < ActiveRecord::Migration def change create_table :default_organizations do |t| t.references :user t.references :relation t.timestamps null: false end end end
class AddIndexToDefaultOrganization < ActiveRecord::Migration def up add_index :default_organizations, :user_id, unique: true add_index :default_organizations, :relation_id end def change add_foreign_key :default_organizations, :user add_foreign_key :default_organizations, :relation end end
この場合も1ファイルで行った時と同様で、テーブル、外部参照、インデックスは作成されますが、 ユニーク属性が付与されません。
インデックス付与だけを別に実行
今度は外部参照までをテーブル作成時に行い、インデックスを別で付与していきます。
class DefaultOrganization < ActiveRecord::Migration def change create_table :default_organizations do |t| t.references :user, foreign_key: true t.references :relation, index: true, foreign_key: true t.timestamps null: false end add_index :default_organizations, :user_id, unique: true end end
この方法だと外部参照かつユニーク属性のインデックスが付与されました!
しかし、rake db:rollback
でロールバックをしようとするとエラーが発生し、ロールバックができません。
これは、外部キー指定のカラムのインデックスを先に削除しようとしているから発生しているからで、
マイグレーションファイルからadd_index :default_organizations, :user_id, unique: true
を削除すると実行されます。
これは、DB側の仕様ですね。
一応設定はできますが、このやり方だとロールバックが発生した際に何で?ってなりますし、
凄く気持ち悪いですね。
解答
紆余曲折しましたが、こちらが解答となります。 マイグレーションファイル1つでできてしまいます。
class DefaultOrganization < ActiveRecord::Migration def change create_table :default_organizations do |t| t.references :user, index: { unique: true }, foreign_key: true t.references :relation, index: true, foreign_key: true t.timestamps null: false end end end
indexの指定の仕方が特殊で、trueではなく { unique: true } となります。
これだけです。
これをマイグレートすると、default_organizationsテーブルが作成され、
user_idとrelation_idに外部キー属性が、user_idはユニークとなります。