[リファクタリングRails]親子関係をポリモーフィック関連に変更

需要がありそうな割にやり方がどこにもなかったのでメモ

# どんなときに有効?

- 後から、共通の子を持つ親modelが必要になったとき


# 例

- 親子関係(has_one)にある User modelとProfile modelがある
- これをポリモーフィック関連に変更して、新しい親model Shopを追加したい

# 現状のコード

```app/models/user.rb
class User < ActiveRecord::Base
  has_one :profile
  accepts_nested_attributes_for :profile
end
```

```app/models/profile.rb
class Profile < ActiveRecord::Base
  belongs_to :user
end
```

# 手順

1. profile model の after_save でポリモーフィック関連のカラムを更新する処理を追加
2. profileテーブルに、ポリモーフィック関連のカラムを追加
3. user, profile model の関連をポリモーフィック関連に変更
4. profileテーブルから、user_id カラムを削除

# 1. profile model の after_save でポリモーフィック関連のカラムを更新する処理を追加

マイグレーションする前に予め追加することで、
マイグレーション直後から、ポリモーフィック関連のカラムが更新されるようにする

```app/models/profile.rb
 class Profile < ActiveRecord::Base
   belongs_to :user
 
   before_save :set_profilable_id
 
   def set_profilable_id
     if self.has_attribute?(:profilable_id)
       self.profilable_id   = self.user_id
       self.profilable_type = 'User'
     end
   end
 end
```

## この時点でのテーブル

```
sqlite> select * from users;
id          name        created_at                  updated_at
----------  ----------  --------------------------  --------------------------
1           1           2016-06-25 16:51:53.577972  2016-06-25 16:51:53.577972
sqlite> select * from profiles;
id          user_id     description  created_at                  updated_at
----------  ----------  -----------  --------------------------  --------------------------
1           1           1            2016-06-25 16:51:53.586769  2016-06-25 16:51:53.586769
```

# 2. profileテーブルに、ポリモーフィック関連のカラムを追加

マイグレーション内で、既存のテーブルのポリモーフィック関連を更新しておく

```
class AddReferenceOnProfile < ActiveRecord::Migration
  def up
    add_reference :profiles, :profilable, polymorphic: true, index: true

    Profile.all.each do |profile|
      if profile.user
        user = profile.user
        profile.profilable_id   = user.id
        profile.profilable_type = user.class.to_s
        profile.save!
      end
    end
  end

  def down
    remove_reference :profiles, :profilable, polymorphic: true, index: true
  end
end
```

## この時点でのテーブル

- ポリモーフィック関連用のカラム `profilable_id` `profilable_type` が追加
- それらに必要な値が格納されている
    - マイグレーション時
    - 追加/更新時 
- `profile.id` の値がおかしいのは、`accepts_nested_attributes_for :profile
` しているのに、 `params.require(:user).permit` に `profile_id` を追加し忘れたため...

```
sqlite> select * from users;
id          name        created_at                  updated_at
----------  ----------  --------------------------  --------------------------
1           1           2016-06-25 16:51:53.577972  2016-06-25 16:51:53.577972
2           2           2016-06-25 16:53:32.988076  2016-06-25 16:53:32.988076
sqlite> select * from profiles;
id          user_id     description  created_at                  updated_at                  profilable_id  profilable_type
----------  ----------  -----------  --------------------------  --------------------------  -------------  ---------------
3           1           1            2016-06-25 16:53:27.580845  2016-06-25 16:53:27.580845  1              User
4           2           2            2016-06-25 16:53:32.990474  2016-06-25 16:53:32.990474  2              User
```

# 3. user, profile model の関連をポリモーフィック関連に変更

```app/models/user.rb
class User < ActiveRecord::Base
  has_one :profile, :as => :profilable
  accepts_nested_attributes_for :profile
end
```

```app/models/profile.rb
class Profile < ActiveRecord::Base
  belongs_to :profilable, polymorphic: true
end
```

form の profile に `profilable_id` を追加

``` app/views/users/_form.html.erb 
  <div class="field">
    <%= f.label :name %><br>
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.fields_for :profile, @user.profile do |p| %>
      <%= p.hidden_field :profilable_id, value: @user.profile.profilable_id %><br>
      <%= p.label :description %><br>
      <%= p.text_field :description %>
    <% end %>
  </div>
```

```app/controllers/users_controller.rb
    def user_params
      params.require(:user).permit(:name, profile_attributes: %i[description profilable_id id])
    end
```

# 4. profileテーブルから、user_id カラムを削除

```
class RemoveUserIdColumnOnProfile < ActiveRecord::Migration
  def change
    remove_column :profiles, :user_id
  end
end
```

# コード

https://github.com/kasei-san/refactor_to_polymorphic