59

I would like set up a polymorphic relation with accepts_nested_attributes_for. Here is the code:

class Contact <ActiveRecord::Base
  has_many :jobs, :as=>:client
end

class Job <ActiveRecord::Base
  belongs_to :client, :polymorphic=>:true
  accepts_nested_attributes_for :client
end

When I try to access Job.create(..., :client_attributes=>{...} gives me NameError: uninitialized constant Job::Client

dombesz
  • 7,890
  • 5
  • 38
  • 47

4 Answers4

64

I've also had a problem with the "ArgumentError: Cannot build association model_name. Are you trying to build a polymorphic one-to-one association?"

And I found a better solution for this kind of problem. You can use native method. Lets look to the nested_attributes implementation, inside Rails3:

elsif !reject_new_record?(association_name, attributes)
  method = "build_#{association_name}"
  if respond_to?(method)
    send(method, attributes.except(*UNASSIGNABLE_KEYS))
  else
    raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
  end
end

So actually what do we need to do here? Is just to create build_#{association_name} inside our model. I've did totally working example at the bottom:

class Job <ActiveRecord::Base
  CLIENT_TYPES = %w(Contact)

  attr_accessible :client_type, :client_attributes

  belongs_to :client, :polymorphic => :true

  accepts_nested_attributes_for :client

  protected

  def build_client(params, assignment_options)
    raise "Unknown client_type: #{client_type}" unless CLIENT_TYPES.include?(client_type)
    self.client = client_type.constantize.new(params)
  end
end
ScotterC
  • 1,054
  • 1
  • 10
  • 23
Dmitry Polushkin
  • 3,283
  • 1
  • 38
  • 44
  • I've used this `build_xyz` trick with success. I think you are missing a line to get `contact_type` from params (and then you need to remove it to the params sent to new) – tokland May 04 '12 at 08:21
  • I may be missing something, but I believe the usage of the word `contact` throughout the last method should actually be changed to `client`... Additionally, I had an issue with two arguments being passed into this method rather than one when build is called. The second argument is a blank hash. – JackCA Aug 20 '12 at 06:30
  • 1
    Thanks @JackCA ! Fixed code and in the lastest Rails 3.2 code were changed, because in the second argument now passes assignment options: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/nested_attributes.rb#L351 – Dmitry Polushkin Sep 05 '12 at 19:36
  • Where's client_type getting defined within #build_client? I ended up hard coding mine to get it to work for now – ScotterC Nov 15 '12 at 21:42
  • `client=` setter should do that. – Dmitry Polushkin Nov 17 '12 at 22:28
  • 7
    In Rails 4.1 I had to change "def build_client(params, assignment_options)" back to "def build_client(params)". – Tatiana Tyu Jun 25 '14 at 16:39
  • Because attr_accessible was removed from the rails core. – Dmitry Polushkin Jun 25 '14 at 19:21
  • 2
    Had to remove `build_client` from `protected` because it did not see the method. – Artem Kalinchuk Jul 01 '17 at 15:26
24

I finally got this to work with Rails 4.x. This is based off of Dmitry/ScotterC's answer, so +1 to them.

STEP 1. To begin, here is the full model with polymorphic association:

# app/models/polymorph.rb
class Polymorph < ActiveRecord::Base
  belongs_to :associable, polymorphic: true

  accepts_nested_attributes_for :associable

  def build_associable(params)
    self.associable = associable_type.constantize.new(params)
  end
end

# For the sake of example:
# app/models/chicken.rb
class Chicken < ActiveRecord::Base
  has_many: :polymorphs, as: :associable
end

Yes, that's nothing really new. However you might wonder, where does polymorph_type come from and how is its value set? It's part of the underlying database record since polymorphic associations add <association_name>_id and <association_name>_type columns to the table. As it stands, when build_associable executes, the _type's value is nil.

STEP 2. Pass in and Accept the Child Type

Have your form view send the child_type along with the typical form data, and your controller must permit it in its strong parameters check.

# app/views/polymorph/_form.html.erb
<%= form_for(@polymorph) do |form| %>
  # Pass in the child_type - This one has been turned into a chicken!
  <%= form.hidden_field(:polymorph_type, value: 'Chicken' %>
  ...
  # Form values for Chicken
  <%= form.fields_for(:chicken) do |chicken_form| %>
    <%= chicken_form.text_field(:hunger_level) %>
    <%= chicken_form.text_field(:poop_level) %>
    ...etc...
  <% end %>
<% end %>

# app/controllers/polymorph_controllers.erb
...
private
  def polymorph_params
    params.require(:polymorph).permit(:id, :polymorph_id, :polymorph_type)
  end

Of course, your view(s) will need to handle the different types of models that are 'associable', but this demonstrates one.

Hope this helps someone out there. (Why do you need polymorphic chickens anyway?)

rodamn
  • 2,191
  • 19
  • 24
  • 1
    Another thing: If an update/patch request modifies the child_type, you need to be extra cautious to maintain database integrity. One solution is to add code preventing or disregarding a request to modify `polymorph_type` in `controller#update` for *existing objects*. I also add the polymorph_type hidden field only when `record_new?` is true. – rodamn Oct 02 '15 at 20:42
  • Great answer. I'd just add that for security reasons, you probably don't want to call `.new` on any arbitrary class that a user can pass in as a String. You could whitelist them or you could come up with another class method name like `.build_as_associable` that only relevant classes implement. – Ritchie Aug 22 '19 at 10:33
8

The above answer is great but not working with the setup shown. It inspired me and i was able to create a working solution:

works for creating and updating

class Job <ActiveRecord::Base
  belongs_to :client, :polymorphic=>:true
  attr_accessible :client_attributes
  accepts_nested_attributes_for :client

  def attributes=(attributes = {})
    self.client_type = attributes[:client_type]
    super
  end

  def client_attributes=(attributes)
    some_client = self.client_type.constantize.find_or_initilize_by_id(self.client_id)
    some_client.attributes = attributes
    self.client = some_client
  end
end
MarkP
  • 171
  • 1
  • 5
  • This is suboptimal to Dmitry / ScotterC's solution below, since Rails anticipated this case and lets you override behavior properly. – Garry Tan Apr 06 '13 at 01:31
  • @MarkP But while updating the same record that has been created, because of overrided `client_attributes`, it creates new record instead of updating it. Do we have any solution for that? – teju c Nov 06 '17 at 07:09
5

Just figured out that rails does not supports this kind of behavior so I came up with the following workaround:

class Job <ActiveRecord::Base
  belongs_to :client, :polymorphic=>:true, :autosave=>true
  accepts_nested_attributes_for :client

  def attributes=(attributes = {})
    self.client_type = attributes[:client_type]
    super
  end

  def client_attributes=(attributes)
    self.client = type.constantize.find_or_initialize_by_id(attributes.delete(:client_id)) if client_type.valid?
  end
end

This gives me to set up my form like this:

<%= f.select :client_type %>
<%= f.fields_for :client do |client|%>
  <%= client.text_field :name %>
<% end %>

Not the exact solution but the idea is important.

dombesz
  • 7,890
  • 5
  • 38
  • 47