Search code examples
ruby-on-railsruby-on-rails-5polymorphic-associations

Polymorphic Associaton transaction fails in Rails 5


I am unable to get my association to save in localhost:3000/controller_name/new. I believe it is due to belongs_to failing validation, but I am not sure how to fix it (beyond just dropping the association via requires:false/optional: true, the consequences of which I am not sure of). I created my association following this tutorial but it was for a previous version of rails.

I have a polymorphic address table that can belong to events, businesses, users, etc. I am trying to add it to event.

Address migration - you can see it references addressable:

class CreateAddresses < ActiveRecord::Migration[5.1]
  def change
    create_table :addresses do |t|     
      t.string :address

      t.decimal :latitude,  null: false, precision: 10, scale: 6, index: true
      t.decimal :longitude, null: false, precision: 10, scale: 6, index: true

      t.references :addressable, polymorphic: true, index: true 
    end
  end
end

Address model:

class Address < ApplicationRecord
    belongs_to :addressable, polymorphic: true
end

Event model:

class Event < ApplicationRecord
    has_one :address, as: :addressable
    accepts_nested_attributes_for :address
end

Event Controller:

class EventsController < ApplicationController
  #...stuff...

  # GET /events/new
  def new
    @event = Event.new
    @event.address = @event.build_address
    #@event.address = Address.new(addressable: @event)
    #@event.address = @event.create_address
    #@event.address =  @addressable.User.new
  end

  #...stuff...

You can see I tried multiple methods to create the event's address, they mostly create the below item, the ones using addressable cause a Nil crash.

#<Address id: nil, address: nil, latitude: nil, longitude: nil, addressable_type: "Event", addressable_id: nil>

Event Form (Uses Simple_form gem):

<%= simple_form_for @event do |f| %>
  <%= f.input :name %>
  <%= f.input :description %>

  <%= f.simple_fields_for :address do |address| %>
    <%= render :partial => 'shared/address/form', :locals => {:f => address} %>
  <% end %>

  <%= f.button :submit %>
<% end %>

Address form partial:

<!-- Google Maps Must be loaded -->
<% content_for :head do %>
    <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCMh8-5D3mJSXspmJrhSTtt0ToGiA-JLBc&libraries=places"></script>
<% end %>

<div id="map"></div>

<%= f.input :address %>
<%= f.input :latitude %>
<%= f.input :longitude %>

Forms render fine. When I try to save I get

Started POST "/events" for 127.0.0.1 at 2017-07-01 16:06:23 -0400
Processing by EventsController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"R0zlSs3UUNV3x8sQn5ocmE4jP12uOsFza7FezBAuhP4sw2MhF1OhixF8sAfDsLpfMEX7x5rhJ9HZfbKna8ncEA==", "event"=>{"name"=>"asd", "description"
=>"asd", "address_attributes"=>{"address"=>"asd", "latitude"=>"1", "longitude"=>"1"}}, "commit"=>"Create Event"}
   (0.1ms)  begin transaction
   (0.1ms)  rollback transaction

And I am kept on the new page. If I insert a byebug into create and print out @event.errors it shows:

#<ActiveModel::Errors:0x007fb29c34a9a8 @base=#<Event id: nil, name: "asd", description: "asd", min_users: nil, max_users: nil, start_time: nil, recurring: nil, created_at: nil, upd
ated_at: nil>, @messages={:"address.addressable"=>["must exist"]}, @details={:"address.addressable"=>[{:error=>:blank}]}>

How can I create the address.addressable? What are the consequences of turning off the requires validation as some SO answers suggest?


Solution

  • Discovered the issues and wrote a blog post discussing this in depth. Essentially I was encountering 2 separate problems

    1.The error I was receiving - "address.addressable"=>["must exist"]. address.addressable is the 'parent' table's tuple ID. In my case it is the Event ID. This error is trying to tell me that this foreign key is missing from the new address when we try to save it in the controller's create function. Like I said in the question, you can set optional:true to ignore this problem, and it somehow magically gets filled in after saving the Event. Or you can manually assign it in the create function before it saves.

    def create
      @event = Event.new(event_params)
      @event.address.addressable = @event #<<<<<<<<<<< manually assign address.addressable
      respond_to do |format|
        if @event.save #Saves the event. Addressable has something to reference now, but validation does not know this in time
          format.html { redirect_to @event, notice: 'Event was successfully created.' }
          format.json { render :show, status: :created, location: @event }
        else
          #dont forget to remove the 'byebug' that was here
          format.html { render :new }
          format.json { render json: @event.errors, status: :unprocessable_entity }
        end
      end
    end
    

    2.Event ID was a string. In my address migration I was using t.references :addressable, polymorphic: true, index: true which is an alias for and addressable_id of integer type. I needed to change my migration to use string IDs - so delete the references line and add this in instead.

    t.string :addressable_id, index: true
    t.string :addressable_type, index: true
    

    This is covered in slightly more detail in the blog post.