需要がありそうな割にやり方がどこにもなかったのでメモ
どんなときに有効?
- 後から、共通の子を持つ親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
手順
- profile model の after_save でポリモーフィック関連のカラムを更新する処理を追加
- profileテーブルに、ポリモーフィック関連のカラムを追加
- user, profile model の関連をポリモーフィック関連に変更
- 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
コード