Rails Accepts_nested_attributes for polymorphic relation solution
Saturday 31 December 2022

#ruby

yesterday i faced a problem with my rails application, my models are as follows

1class Vehicle < ActiveRecord::Base
2  MOVABLES = [:car, :bus, :plane]
3  belongs_to :movable, polymorphic: true, dependent: :destroy, required: true, autosave: true
4  accepts_nested_attributes_for :movable
5end
1class Car < ActiveRecord::Base
2  has_one :vehicle, dependent: :destroy, as: :movable
3end
1class Bus < ActiveRecord::Base
2  has_one :vehicle, dependent: :destroy, as: :movable
3end
1class Plane < ActiveRecord::Base
2  has_one :vehicle, dependent: :destroy, as: :movable
3end

so, when I try to create a vehicle with nested attributes for the movable it complains
with error “are you trying to build a polymorphic relation?”

1Vehicle.create! name: 'my car', movable_type: 'Car', movable_attributes: { color: red }

the previous code should create a vehicle with extended attributes of a car with color = red,
so i had to google first and guess what is the first thing to find?, Yup it is Stackoverflow

the highly rated solution didn’t work for me :

 1class Job <ActiveRecord::Base
 2  belongs_to :client, :polymorphic=>:true
 3  attr_accessible :client_attributes
 4  accepts_nested_attributes_for :client
 5
 6  def attributes=(attributes = {})
 7    self.client_type = attributes[:client_type]
 8    super
 9  end
10
11  def client_attributes=(attributes)
12    some_client = self.client_type.constantize.find_or_initilize_by_id(self.client_id)
13    some_client.attributes = attributes
14    self.client = some_client
15  end
16end

so i tried this solution

 1class Job <ActiveRecord::Base
 2  belongs_to :client, :polymorphic=>:true, :autosave=>true
 3  accepts_nested_attributes_for :client
 4
 5  def attributes=(attributes = {})
 6    self.client_type = attributes[:client_type]
 7    super
 8  end
 9
10  def client_attributes=(attributes)
11    self.client = eval(type).find_or_initialize_by_id(attributes.delete(:client_id)) if client_type.valid?
12  end
13end

and i had to modify it to suite my code like so

 1class Vehicle < ActiveRecord::Base
 2  MOVABLES = [:car, :bus, :plane]
 3  belongs_to :movable, polymorphic: true, dependent: :destroy, required: true, autosave: true
 4  accepts_nested_attributes_for :movable
 5
 6  def attributes=(attributes = {})
 7    self.movable_type = attributes[:movable_type]
 8    super
 9  end
10
11  def movable_attributes=(attributes)
12    self.movable = eval(type).find_or_initialize_by_id(attributes.delete(:movable_id)) if movable_type.valid?
13  end
14end

but yeah, that didn’t work either so i got the idea from the previous code movable_attributes= is the method that is invoked
when assigning the the mobable_attributes hash, so i have to override it to create/update the current child as follows

 1class Vehicle < ActiveRecord::Base
 2  MOVABLES = [:car, :bus, :plane]
 3  belongs_to :movable, polymorphic: true, dependent: :destroy, required: true, autosave: true
 4  accepts_nested_attributes_for :movable
 5
 6  def movable_attributes=(attributes)
 7    if MOVABLES.include?(movable_type.underscore.to_sym)
 8      self.movable ||= self.movable_type.constantize.new
 9      self.movable.assign_attributes(attributes)
10    end
11  end
12end

that will create a movable object if it doesn’t exist, and it’ll update it in case it
already exists.

i wish rails would behave like this by default, and i have no idea why it doesn’t do that as it is
a trivial solution.

Backlinks

See Also