マイグレーション作成時のチェックポイント
日頃、Railsのマイグレーションコードを書いたりレビューするときに気にしているポイントを紹介したいと思います。
マイグレーションそのものに関するポイント
1. バージョンを下げることができることを確認する
基本的には、まず作ったmigrationを実行して上げてみます。
> rake db:migrate
ここで終わらずに、続けて redo をし、下げて上げます。こうすることで、down側に基本的なミスがないことを確認できます。
> rake db:migrate:redo
2. アプリケーションで定義したクラスを直接使わない
app/models 下のモデルクラスなど、マイグレーションファイルの外部に定義している、将来実装を変更する可能性のあるクラスを直接利用することは禁じ手と考えた方がいいでしょう。
なぜかというと、マイグレーションファイルというのは、未来にわたって末永く、書いたときの意図どおりに動く 必要があるからです。言い換えれば、マイグレーションファイルのコードは、マイグレーションファイル内で閉じていて、凍結されていることが望ましい のです。
class AddTelToUsers < ActiveRecord::Migration
def change
add_column :users, :tel, :string, default: User::DEFAULT_TEL
end
end
このような単純な例でさえ、将来、User::DEFAULT_TEL の値(実装)が変更されてしまったら、その時点で "だいぶ以前の古いマイグレーション" になっているこのコードの挙動も同時に変わってしまいます。マイグレーションコードで過去の定数値を使うように変更する、といった対応をし忘れれば、例えば、本番DBとローカルDBのデフォルト値が違った状態になったり、後続のマイグレーションの前提が狂って思わぬエラーやデータ不整合を招くことになるかもしれません。
データのメンテナンスなどのために、マイグレーションでActiveRecordクラスを使いたくなることも、ままあります。個人的には、間違えやすいため基本的に使わない方が良いと考えていますが、どうしても使いたい場合は、マイグレーションファイル内で定義をするとよいでしょう。誤読や、たまたま実在クラスのほうを呼べてしまって動く事故を避けるため、実在しない(将来も実在しなさそうな)クラス名をつけておくのも良いと思います。
class DeleteDeletedFromUsers < ActiveRecord::Migration
class MigrationUser < ActiveRecord::Base
self.table_name = "users"
end
def up
MigrationUser.where(deleted: true).delete_all
end
...
なお、私は基本的にはActiveRecordクラスは使わず、execute で SQL を実行するようにしています。
3. 既存データを意識する
マイグレーションは、そのマイグレーションを実行するときに、関係する既存データがあるどうかにも気を配る必要があります。たとえば、次のようなマイグレーションは、users テーブルのレコードが1件もない状態では成功しますが、レコードがあれば成功しません。
class AddTelToUsers < ActiveRecord::Migration
def change
add_column :users, :tel, :string, null: false
end
end
4. 不可逆性についてはコメントを書いておく
これも既存データの話になりますが、マイグレーションでは、性質上、バージョンを下げて戻すと、データが復元できないケースがあります。さきほどの delete_all の例などがそうです。気軽にバージョンを戻せるのか、戻せないのかということが簡単にわかると便利なことがあるので、復元できないものがある場合は適宜、コメントを付けておくと良いでしょう。
# 注意:このマイグレーションを行うと、論理削除レコードが削除され、復元できません。
class DeleteDeletedFromUsers < ActiveRecord::Migration
class MigrationUser < ActiveRecord::Base
self.table_name = "users"
end
def up
MigrationUser.where(deleted: true).delete_all
end
...
なお、注意を喚起するよりも、バージョンを下げることを禁止したいということもあるかもしれません。その場合は次のように例外を投げることで、禁止することができます。
def down
raise ActiveRecord::IrreversibleMigration
end
禁止するか、コメントでの注意喚起にとどめるかは、マイグレーションのバージョンを戻せる利点と、望まないDB状態になることを強制的に防げる利点のどちらが大きいかで判断すると良いでしょう。(詳しくはコメント欄をご参照ください。)
5. 過去のマイグレーションファイルはなるべく変更しない
エラーの修正、Rubyのバージョンアップ、禁じ手の外部利用クラスの仕様変更対応、などの特異な状況を除いて、過去のマイグレーションファイルはなるべく変更すべきでありません。正しくは、シェアされた過去のマイグレーションファイル を変更すべきではありません。
これは、ほかの開発者がその過去のマイグレーションファイルを利用している場合に、DBの状態を修正するのがとても面倒になるからです(自分自身も、十分バージョンを戻してから変更をしないと同様の面倒な状態になります)。
従って、自分だけが使っている開発中のブランチにしかまだ存在しないマイグレーションを変更するといったことは問題ありません。
また、スキーマやデータに影響ない修正は、あまり問題になりません。
なお、本当に必要があれば過去のマイグレーションファイルの変更をすることも仕方がありません。このときに、その変更をほかの開発者が取り入れないといけない場合には、しっかりアナウンスをして、DB状態に関する混乱がなるべく発生しないように立ち回りましょう。
6. マイグレーションファイルをテーブル定義ファイルとして利用しない
Rails経験の浅いチームでは、マイグレーションファイルは1つのテーブルを create_table するという形であるべきだと考えてしまうことがあります。
開発の初期は、1つ1つテーブルを作っていくことが多いので、マイグレーションファイルはテーブルと1対1になり、テーブルの定義として利用することができ、ある種の美しさがあります。
class CreateCompanies < ActiveRecord::Migration
def change
create_table :companies do |t|
t.string :name
t.string :address
t.timestamps
end
end
end
このため、この美しさを保ちたい、という要求が生まれることがあるのです。この欲求を満たそうとすると、各テーブルへの変更要求が生じるたびに、対応する過去の create_table のあるマイグレーションファイルを変更するという状況になります。過去のマイグレーションファイルの変更は、開発手順に大きなロスを与えます。ましてや、このような使い方では、それが日常的に激しく発生することになります。
マイグレーションはそもそも、DBへの変更差分を都度記述しておいて、複数の開発者間で共有し、それぞれの開発者がまだ当てていない差分を都合のよい時に当てる、ということを簡単にする仕組みです。
マイグレーション = create_table という状態を守ることに情熱を注ぐのは、変更差分を都度記述せず、結果だけを記述するということと同じです。すなわち、Railsのマイグレーションを使わないということと同じです。
過去のマイグレーションの美しさに執着せず、小さい変更であっても、確実に新しいマイグレーションを積んでいくようにしましょう。
DB設計に関するポイント
7. 外部キーについて検討する
Webアプリケーションでは、外部キーについて正規化を行うよりも、特定のキー、たとえばマルチテナントのアプリケーションであればテナントのidなどを、多くのテーブルに持たせたほうが見通しがよい場合があります。テーブルを作成するときは、JOINして辿れはするけれど直接持ったほうが良いかもしれないカラムを冗長に保持する可能性を検討するとよいでしょう。
また、Rails 4.2から、マイグレーションで簡単に外部キー制約を指定することが可能になったので、利用を検討しましょう。外部キーのidが必ず存在することをDBレベルで保証することができます。
def change
add_foreign_key :documents, :tenants
end
8. インデックスを張る
マイグレーションではインデックスを張ることができます。テーブルを作るときに、あわせてインデックスについても検討し、定義しておくとよいでしょう。
create_table :documents do |t|
t.index :tenant_id, :user_id
インデックスは、テーブル作成時に検討を忘れがちなので、ステップとして意識することをお勧めします。
9. ユニークインデックスについて検討する
キー(の組み合わせ)が一意になる場合には、ユニークインデックスを張ると、二度押しなどで意図せずレコードが作られることをDBレベルで防止できて便利です。
create_table :taggings do |t|
t.index :taggable_id, :tag_id, unique: true
10. NULL不可をつけられるときはつける
テーブル作成やカラム追加のマイグレーションにおいてもっとも漏れやすいことのひとつが、NULL不可の指定です。
特に、boolean のカラムについては、NULL不可にすべき場合が多いので、意識して付けるようにするとよいでしょう。
なお、NULL不可にすると、データなしでのinsert等ができなくなるので、デフォルト値をつけられるときは合わせてなるべく付けるようにすると、扱いが楽になります。
add_column :users, :deleted, :boolean, default: false, null: false
余談ですが、default をtypoしてもエラーは出ないので、typoに気をつけましょう。
11. Ruby的な良い名前をつける
Railsでは、カラム名をそのまま属性名として利用します。DBでは、かつてはプリフィックス等をつけた重厚な名前も一般的でしたが、Railsで利用する場合は、Rubyの属性として使える短めのわかりやすい名前をつけると良いでしょう。
"MN_NAME_STR" などではなく "name" にするといった具合ですね。
12. typeというカラム名を避ける
typeというカラム名は、ActiveRecordのSTI(単一テーブル継承)で利用するカラム名となっており、ほかの名前のように使うことができません。避けましょう。
category, kind といった逃げ道があります。
ちなみに、xxx_type という名前は利用可能ですが、これはポリモーフィック関連を示唆する名前であるため、誤解を招かないように、避けておくのが無難です。