Rails + SQLiteでカラム定義変更時にエラーが出た場合の対処法

# 起こったこと
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制約を追加するマイグレーションファイルを作成。

```Shell
$ rails g migration change_users_name_to_not_null
```

マイグレーションファイルを編集。

```Ruby:yyyyMMdd_change_users_name_to_not_null.rb
class ChangeUsersNameToNotNull < ActiveRecord::Migration[5.2]
  def change
    change_column_null :users, :name, false
  end
end
```

マイグレーションを実行。

```Shell
$ rails db:migrate
```

ここで冒頭のエラーが発生。

```
SQLite3::ConstraintException: FOREIGN KEY constraint failed: DROP TABLE "tables"
```


# 解決手順
解決は以下の手順で行う。

**1. データのバックアップを取得する**
**2. バックアップファイルを編集する**
**3. データを削除する**
**4. マイグレーションを実行する**
**5. バックアップデータを元に戻す**

## 1. データのバックアップを取得する
profilesテーブルがusersテーブルを参照していたので、profilesテーブルのバックアップを取得する。

### SQLiteのコンソールを起動する

```Shell
$ 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`に置換する。

**変更前**

```SQL:insert_profiles.sql
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');
```

**変更後**

```SQL:insert_profiles.sql
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のコンソールを起動する

```Shell
$ 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だったので、問題なくマイグレーションを実行できました。


# 参考リンク
[sqlite3でカラム定義の変更 - hokaccha memo](https://hokaccha.hatenablog.com/entry/2016/10/13/205504)
[SQLiteコマンドの使い方 | SQLite入門](https://www.dbonline.jp/sqlite/sqlite_command/)