Polymorphic Associations in Rails 4, Part 2
Pain point
This is a follow-up on Part 1, primarily to address a common headache with creating/updating polymorphic models with a single route. It starts with an error:
Error: Cannot build association medium. Are you trying to build a polymorphic one-to-one?
Relief
Briefly, here’s our starting point (see Part 1 for more details on arriving at this configuration):
# painting.rb class Painting < ActiveRecord::Base belongs_to :medium, polymorphic: true accepts_nested_attributes_for :medium end # water_painting.rb class WaterPainting < ActiveRecord::Base # For your sanity, you may want to use # :medium instead of :details has_one :painting, as: :details, dependent: :destroy end # oil_painting.rb class OilPainting < ActiveRecord::Base # For your sanity, you may want to use # :medium instead of :details has_one :painting, as: :details, dependent: :destroy end # your_paintings_controller.rb # ... snip ... # GET /your_paintings/water/new def new_water @painting = Painting.new(medium: WaterPainting.new) end # ... snip ... # GET /your_paintings/oil/new def new_oil @painting = Painting.new(medium: OilPainting.new) end # ... snip ...
In all likelihood, since you have different types of paintings, they will have different attributes/fields and you’ll want to be able to render different views for each.
The trouble comes when we want to have a generic create method/route for creating any type of painting:
# your_paintings_controller.rb # POST /your_paintings/create def create # This will throw an error! @painting = Painting.new your_painting_params respond_to do |format| if @painting.save format.html { redirect_to your_painting_path(@painting.id), notice: 'Your painting was successfully created.' } else format.html { render :new } end end end # ... snip ... def your_painting_params params.require(:painting).permit! end
The error is thrown when the POST request comes back to the server (running the generic create method we have above), and there’s no state on the server or in the params that Rails can use to determine which type of painting you intended to create.
I wanted to avoid having n create
handlers (n being the total number of painting types) that were basically identical, and realized that I just needed to get the desired type communicated to the POST handler and everything thing could be DRYed out.
I updated each painting type’s new
view with the following hidden field:
# new_water.html.erb <h1>New Water Painting</h1> <%= simple_form_for @painting, url: { controller: "your_paintings", action: "create" } do |f| %> <%= f.input :title %> <%= f.simple_fields_for :medium do |v| %> <%= v.input :source %> <% end %> # The all-important medium_type! <%= f.hidden_field :medium_type %> <%= f.button :submit %> <% end %>
To leverage that field value on the server, I used the following approach to safely create the correct painting type:
# your_paintings_controller.rb # All the different painting types' models: PAINTING_TYPES = [WaterPainting, OilPainting] # ... snip ... def create begin medium_klass = PAINTING_TYPES.detect { |m| your_painting_params[:medium_type].classify.constantize == m } ensure unless medium_klass redirect_to your_paintings_water_path, alert: "Incorrect painting type (klass)" and return end end # Can't use these bad boys yet, Rails thinks # you're just adding random extra attrs to the # base Painting model, and doesn't like it much. # Save them for later though: medium_params = your_painting_params.delete :medium_attributes @painting = Painting.new your_painting_params # Ah! Now bring in the painting type's attrs: @painting.medium = medium_klass.new medium_params respond_to do |format| if @painting.save format.html { redirect_to your_painting_path(@painting.id), notice: 'Your painting was successfully created.' } else format.html { render :new } end end end # ... snip ... def your_painting_params params.require(:painting).permit! end
This approach creates the Painting base object, then adds the correct association object type and passes the correct attrs to each.