起こったこと
Railsの開発環境において、とあるカラムにNot Null制約を追加しようとしたらこんなエラーが出てしまった。
SQLite3::ConstraintException: FOREIGN KEY constraint failed: DROP TABLE "tables"
環境
Ruby : 2.6.3
Ruby on Rails : 5.2.3
SQLite3 : 3.24.0
原因
SQLiteではカラム定義の変更ができないようで、Railsでは一時テーブルにデータを退避した上でDrop & Createしている。
そのため、変更対象テーブルを外部キー制約で参照しているテーブルにデータが存在すると、上述のエラーが発生してマイグレーションが失敗してしまう。
今回はすでにテストデータが存在していたため、このようなエラーが発生してしまった。
解決策
変更対象テーブルを参照しているテーブルは1つのみで、そのテーブルは他のテーブルから参照されていなかった。
そこで、参照テーブルのデータのバックアップをとってから一旦削除し、マイグレーション実行後にデータを元に戻すことで対応した。
エラーが発生するまでの流れ
カラムにNot Null制約を追加するマイグレーションファイルを作成。
$ rails g migration change_users_name_to_not_null
マイグレーションファイルを編集。
class ChangeUsersNameToNotNull < ActiveRecord::Migration[5.2]
def change
change_column_null :users, :name, false
end
end
マイグレーションを実行。
$ rails db:migrate
ここで冒頭のエラーが発生。
SQLite3::ConstraintException: FOREIGN KEY constraint failed: DROP TABLE "tables"
解決手順
解決は以下の手順で行う。
1. データのバックアップを取得する
2. バックアップファイルを編集する
3. データを削除する
4. マイグレーションを実行する
5. バックアップデータを元に戻す
1. データのバックアップを取得する
profilesテーブルがusersテーブルを参照していたので、profilesテーブルのバックアップを取得する。
SQLiteのコンソールを起動する
$ rails db
.mode
でSQL実行結果の出力形式を変更する
insertを指定するとINSERT文が出力されるようになる。
sqlite> .mode insert
.output
でSQL実行結果の出力先を変更する
ファイルパスを指定するとファイルに出力されるようになる。
sqlite> .output ../backup/insert_profiles.sql
バックアップを取得したいテーブルをSELECTする
これでデータのINSERT文がファイルに出力される。
sqlite> select * from profiles;
SQLiteのコンソールを終了する
sqlite> .exit
2. バックアップファイルを編集する
テーブル名を置換する
テーブル名がtables
になっているので、profiles
に置換する。
変更前
INSERT INTO "table" VALUES(1,1,'2019-06-28 13:55:06.499206','2019-06-28 13:55:06.499206');
INSERT INTO "table" VALUES(2,2,'2019-06-28 15:22:26.330312','2019-06-28 15:22:26.330312');
INSERT INTO "table" VALUES(3,3,'2019-06-28 15:37:37.181411','2019-06-28 15:37:37.181411');
変更後
INSERT INTO "profiles" VALUES(1,1,'2019-06-28 13:55:06.499206','2019-06-28 13:55:06.499206');
INSERT INTO "profiles" VALUES(2,2,'2019-06-28 15:22:26.330312','2019-06-28 15:22:26.330312');
INSERT INTO "profiles" VALUES(3,3,'2019-06-28 15:37:37.181411','2019-06-28 15:37:37.181411');
3. データを削除する
アプリケーション側との不整合が発生すると怖いので、念のためRailsのコンソールから行う。
Railsのコンソールを起動する
$ rails c
バックアップを取得したテーブルのデータを全て削除する
irb> Profile.all.destroy_all
念のため削除されているか確認する
結果が空であればOK。
irb> Profile.all
Railsのコンソールを終了する
irb> exit
4. マイグレーションを実行する
マイグレーションを実行する
正常に実行されればOK。
$ rails db:migrate
5. バックアップデータを元に戻す
再びSQLiteのコンソールを起動する
$ rails db
.read
でファイルに出力したINSERT文を実行する
INSERT文のテーブル名を修正し忘れているとエラーが出るので注意。
sqlite> .read ../backup/insert_profiles.sql
念のため登録されているか確認する
元のデータが表示されればOK。
sqlite> select * from profiles;
SQLiteのコンソールを終了する
sqlite> .exit
まとめ
これでマイグレーションを実行した上でデータも元通りになりました。
今回はテーブル同士の参照が少なかったためササッと解決できましたが、参照が複雑に絡み合っていたりすると大変そうですね。
ちなみに本番環境はMySQLだったので、問題なくマイグレーションを実行できました。