Rails 5.1以前に作成されたテーブルにFK制約を張れない問題の解決策

# Rails 5.1のmigrationに関する変更点

Rails 5.1以前はmigrationを作成するとデフォルトでidカラムはintで作成されますが5.1からはこれがbigintに変更されました。
[Restore the behaviour of the compatibility layer for integer-like PKs #27389](https://github.com/rails/rails/pull/27389)

この変更で以下のようなmigrationが既に存在している場合に、

```ruby:db/migrate/20171225000001_create_hoges.rb
class CreateHoges < ActiveRecord::Migration[5.0]
  def change
    # hoges.idはintで作成される
    create_table :hoges do |t|
      t.string :name

      t.timestamps
    end
  end
end
```

Rails5.1以降にアップデートするとFK制約を張れなくなりました。

```ruby:db/migrate/20171225000101_create_piyos.rb
class CreatePiyos < ActiveRecord::Migration[5.1]
  def change
    create_table :piyos do |t|
      # piyos.hoge_idはbigintで作成される
      t.references :hoge, foreign_key: true

      t.timestamps
    end
  end
end
```

`hoges.id` はintで作成されますが `piyos.hoge_id` はbigintで作成されるので、別のタイプのカラム同士ではFK制約を張れないので `rails db:migrate` が失敗します。

この問題の解決策を2つ紹介します。


# hogesテーブルのidを変更せずにintでpiyos.hoge_idを作成する

## piyos.hoge_idをintで作成する

```ruby:db/migrate/20171225000101_create_piyos.rb
class CreatePiyos < ActiveRecord::Migration[5.1]
  def change
    create_table :piyos do |t|
      # piyos.hoge_idをintで作成する
      t.references :hoge, foreign_key: true, type: :integer

      t.timestamps
    end
  end
end
```

もしくは

## piyos.hoge_idをintにして別途FK制約を張る

```ruby:db/migrate/20171225000101_create_piyos.rb
class CreatePiyos < ActiveRecord::Migration[5.1]
  def change
    create_table :piyos do |t|
      # piyos.hoge_idをintで作成する
      t.integer :hoge_id, default: nil

      t.timestamps
    end
    # 別途FK制約を作成する
    add_foreign_key :piyos, :hoges
  end
end
```

### add_foreign_keyについて

add_foreign_keyは名前の通りFK制約を作成します。
最初の引数で参照元のテーブル名、次の引数で参照先のテーブル名を指定します。
オプションとしてcolumnで参照元のカラム名を、primary_keyで参照先のカラム名を指定できます。
他のオプションとして参照先更新及び削除時の挙動を変更するon_updateとon_deleteもありますが、ここでは扱いません。

つまり、下の2つは同義です。

```ruby
add_foreign_key :piyos, :hoges
add_foreign_key :piyos, :hoges, column: :hoge_id, primary_key: :id
```

#### piyos.hoge_idをpiyos.foo_idにしたい場合

```ruby
add_foreign_key :piyos, :hoges, column: :foo_id
```

とすればOKです。


#### another_id等がPRIMARY KEYになってしまっている場合

```ruby:db/migrate/20171225000001_create_hoges.rb
class CreateHoges < ActiveRecord::Migration[5.0]
  def change
    create_table :hoges, id: false do |t|
      t.string :name
      t.column :another_id, 'INTEGER PRIMARY KEY AUTO_INCREMENT'

      t.timestamps
    end
  end
end
```

以下のようにすればOKです。

```ruby:db/migrate/20171225000101_create_piyos.rb
class CreatePiyos < ActiveRecord::Migration[5.1]
  def change
    create_table :piyos do |t|
      t.integer :hoge_another_id

      t.timestamps
    end
    add_foreign_key :piyos, :hoges, column: :hoge_another_id, primary_key: :another_id
  end
end
```

__id以外をPRIMARY KEYにしたりPRIMARY KEY以外にFK制約を張るのはRails wayから外れるのでやむを得ない事情があっても控えましょう。__


# hogesテーブルのidをintからbigintに変更してからpiyosを作成する

## hoges.idをintからbigintに変更する

```ruby:db/migrate/20171225000002_change_hoges_id.rb
class ChangeHogesId < ActiveRecord::Migration[5.1]
  def up
    # intからbigintへの変更用
    change_column :hoges, :id, :bigint, auto_increment: true
  end
  def down
    # rollback時のbigintからintへの変更用
    change_column :hoges, :id, :int, auto_increment: true
  end
end
```

## 普通にpiyosを作成する

```ruby:db/migrate/20171225000002_change_hoges_id.rb
class CreatePiyos < ActiveRecord::Migration[5.1]
  def change
    create_table :piyos do |t|
      t.references :hoge, foreign_key: true

      t.timestamps
    end
  end
end
```

# 終わりに

いかがだったでしょうか、Railsは基本後方互換性を保ちながら開発が進められていますが、Webの世界は流れが早いのでこういった破壊的な変更が稀に起こるのは仕方のない事でしょう。

この記事によってRDBにFK制約を張らずにModel側でのみassociationを設定してしまうRailsエンジニアが1人でも減る事を祈ります。