Railsにおいてmigrationはリバーシブルに設計するべき
背景
Railsの開発で外部キーを追加したあとに削除を行った際に、Rubocopで指摘があり
解決するにあたって色々調べたため内容を書き留めておきたいと思いました。
本記事では、Rubocopの指摘とMigrationに関する説明を簡単に解説していきます。
情報に関しては適当にフェイク入れていますので、ご注意ください
概要
既存のデータベースの外部キーを削除したいケースがあり、以下のmigrationを作成しました。
以下はclubsテーブルから外部キーを削除するmigrationファイルです。
class RemoveUserIdFromClubs < ActiveRecord::Migration[6.1] def change remove_column :clubs, :user_id end end
change_tableメソッド
change_table
メソッドは、既存のテーブルのスキーマ変更を定義するために使用されます。
既存のテーブルの名前を引数に取り、そのテーブルに対してカラムの追加、変更、削除、インデックスの追加、インデックスの削除などのスキーマ変更を定義するために使用されます。
rubocopの指摘
上記では単純にchange_tableで既存のidカラムを削除したmigrationを作成しただけですが、カラムの削除はrails db:migrate
をで問題なく動作していましたがrubocopでエラーが発生しました。
rubocopを実行したときに発生したエラーは以下です。
db/migrate/20230405000000_remove_user_id_from_clubs.rb:5:5: C: Rails/ReversibleMigration: remove_column(without type) is not reversible. remove_column :clubs, :user_id ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 717 files inspected, 1 offense detected
問題点: 現状のやり方だとロールバックができない
このエラーメッセージは、マイグレーションファイルにある remove_column
メソッドが、逆のマイグレーションでロールバックできない非可逆的な操作であることを示しています。
つまり、このままでは rails db:rollback
コマンドを実行しても、user_id
カラムをテーブルに再度追加することができません。
後で削除したカラムを追加することができなくなってしまうため問題があります。
確認
試しに**rails db:rollback**
を実行したところ以下のようにエラーが発生しました。
# 実行 $ rails db:rollback # 結果 == 20230405000000 RemoveUserIdFromClubs: reverting ========================= rails aborted! StandardError: An error has occurred, all later migrations canceled: remove_column is only reversible if given a type. /Users/yano/workspace/vendor/bundle/ruby/3.2.0/gems/activerecord-6.1.7.2/lib/active_record/migration/command_recorder.rb:180:in `invert_remove_column'
解説
以下はclubsテーブルからuser_idを削除することはわかります。
def change remove_column :clubs, :user_id end
ただ、migrationファイルにはrollbackという機能があるため、真逆の処理も考慮してプログラムを書かないと動きません。
remove_columnの逆はadd_columnですが上記のファイルを元にadd_columnをすると必須である型の指定がないことがわかります。
def change add_column :clubs, :user_id # 型は? end
どんな型でカラムを作ればいいかの情報が上記では推測できないのでreversibleではなく、エラーが発生していました。
解決
このエラーを解決するためには、ロールバック可能な方法でカラムを削除する必要があります。
def change remove_column :clubs, :user_id, :bigint # 型を指定してあげる end
このように書くと、rails db:rollback
コマンドを実行すると、add_column
メソッドで逆の変更を行うロールバック用のマイグレーションが自動的に生成されます。
ロールバックが成功しています。ログを見るとremoveのロールバックではrevertが実行されていること
&add_columnメソッドでは型が指定されている
ことがわかります。
# 実行 rails db:rollback # 結果 == 20230405000000 RemoveUserIdFromClubs: reverting ========================= -- add_column(:clubs, :user_id, :bigint) -> 0.3051s == 20230405000000 RemoveUserIdFromClubs: reverted (0.3052s) ================
rollbackができることがわかったため元に戻しておきます。
# 実行 rails db:migrate # 結果 == 20230405000000 RemoveUserIdFromClubs: migrating ========================= -- remove_column(:clubs, :user_id, :bigint) -> 0.0954s == 20230405000000 RemoveUserIdFromClubs: migrated (0.0955s) ================
おまけ
upメソッドとdownメソッド
このように修正することでも、 up
メソッドでカラムを削除することができ、 down
メソッドでカラムを再度追加することができます。
class RemoveUserIdFromClubs < ActiveRecord::Migration[6.1] def up remove_column :clubs, :user_id, :bigint end def down add_column :clubs, :user_id, :bigint end end
rubocop / rails-style-guide - Reversible Migration
上記のrubocopのコードを参照し今回のケースに合わせています。時間ある際にはこのような情報も一読しておくと理解が深まりそうです。
remove_columnとt.removeの違い
remove_column
とt.remove
は、機能的に同じですが、使用方法が異なります。
change_table
メソッドは、カラムの追加や変更、インデックスの追加や変更など、テーブル全体の変更を実行する場合に便利です。また、change_table
メソッドは、複数のカラムの変更をまとめて実行することができるため、コードの見通しを良くすることができます。
def change change_table :clubs do |t| t.remove :user_id, :bigint end end
remove_column
は、change
メソッドの中で使用され、直接カラムを削除します。
単一のカラムの削除に適していることがわかります。change_tableのブロックでネストしなくて良いためコードが簡潔になり、読みやすくなります。
class RemoveUserIdFromClubs < ActiveRecord::Migration[6.1] def change remove_column :clubs, :user_id, :bigint end end
おわりに
コメント
本記事の内容は以上になります!
migrationのメソッドについてやrollbackなどの処理の内容やルールについて見直すきっかけになりました。
参考になったり学びのきっかけになりますと幸いです。
間違いがありましたら修正いたしますので、ご指摘ください。
興味があれば他の記事も更新していきますので是非ご覧になってください♪
プログラミングスクールのご紹介 (卒業生より)
お世話になったプログラミングスクールであるRUNTEQです♪
ご不明な点ありましたらお気軽にコメントか、TwitterのDMでお答えします♪