5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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

どんなときに有効?

  • 後から、共通の子を持つ親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).permitprofile_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

コード

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?