P

olymorphic Associations help when one table must belongs_to multiple other tables. The Rails docs demonstrate this relationship with an Imageable relationship:

Polymorphic Imageable tables

Another common example is an Address, without polymorphic behavior it would need to belong to every possible table, and would lead to a lot of wasted DB space and bad code.

1 2 3 4 5 6 class Address < ApplicationRecord belongs_to :person belongs_to :company belongs_to :event #MORE and MORE belongs_to end

Polymorphic associations allow us to have much cleaner code and databases.

1 2 3 class Address < ApplicationRecord belongs_to :addressable end

In this article we'll show you how to set up migrations, models, and forms for a simple polymorphic association - a has_one address.

People have a lot of issues with Polymorphic associations - 1, 2, 3, 4, etc. Many of those questions do not have an accepted answer, despite the seeming abundance of guides on the matter. There are a few gotchas in Rails 5, we will cover them in depth.

So without further ado, let's begin:

1 2 rails g scaffold Events name :string rails g model Address address :string addressable :references { polymorphic }

Migrations

Open the new db/migrate/***_create_addresses.rb migration and edit the 'references' line to add in an index.

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

References is an alias to creating both a foreign key and a type column, made specifically for polymorphic behavior. The foreign key links to the 'parent' table that will create an Adress. It is equivalent to:

1 2 t . integer :addressable_id #foreign key t . string :addressable_type #type

addressable_id references the "parent" association that has_one , has_many , etc of Addresses. For us, this will be an Event id. addressable_type saves the name of the parent, for us it'll be "event".

With that our migration is done! However, before we move on, check out this picture again.

Polymorphic Imageable tables

A subtle yet extremely important takeaway is that imageable_id, product's id, and employee's id are all integers. There are lots of good reasons to use string ids, and you might want to use one too. If one parent table has a string ID, the polymorphic foreign key (imageable_id) and all other parent tables in that association must also have string ids.

For example, if we changed Employee's ID to a string, we would also need to change Product's ID and Picture's imageable_id . Furthermore, this would prevent us from being able to use references in the Imageable migration (since it aliases an integer). You would need to manually define the two columns:

1 2 t . string :imageable_id , index: true t . string :imageable_type , index: true

Now our migrations are looking good. Time to migrate!

1 rake db :migrate

Models

The Address model is already OK, and will contain a nice belongs_to :addressable line. The Event model must be edited to create the address association, and to accept nested attributes for the address in its form:

1 2 3 4 class Event < ApplicationRecord has_one :address , as: :addressable accepts_nested_attributes_for :address end

Views

I really like simple-forms so let's go ahead and install that before mucking around in Rails forms. Add gem 'simple_form' to the Gemfile and bundle install . Open app/views/events/_form.html.erb and replace the file with a simple_form.

1 2 3 4 5 6 7 8 9 < %= simple_form_for @event do |f| %> <%= f . input :name %> < %= f.simple_fields_for :address do |address| %> <%= render partial: 'shared/address/form' , locals: { f: address } %> < % end %> < %= f.button :submit %> <% end %>

This file contains a nested form (via simple_fields_for ) which is rendered from a partial. Partials allow us to maintain a single point of control over address forms even though they might be used in multiple parent forms.

Let's create the partial. Make a new file and name it app/views/shared/address/_form.html.erb . This file is rather simple:

1 < %= f.input :address %>

The forms are all done!

Let's ensure the address will be displayed when we are inspecting a given Event. Open app/views/events/show.html.erb and add the following display code somewhere in the file.

1 2 3 4 5 6 <!-- other stuff... --> <p> <strong> Address: </strong> <%= @event . address . address %> </p> <!-- other stuff... -->

Controllers

Finally, let's string everything together in the event controller - app/controllers/events_controller.rb . First we must modify the strong params to accept the nested address fields, so the event_params function must be edited.

1 2 3 4 # other stuff... def event_params params . require ( :event ). permit ( :name , { address_attributes: [ :address ] } ) end

Finally, an address must be built when the new action is called. This ensures the address's fields will be displayed in our event form.

1 2 3 4 5 # GET /events/new def new @event = Event . new @address = @event . build_address end

Seems like we're done right? Not quite.

Troubleshooting

If you run your project ( rails s ), navigate to /events/new , and attempt to create an event it won't work. On the the terminal running rails, you'll see something like this

1 2 3 4 5 Started POST "/events" for 127.0.0.1 at 2017-07-13 14:52:27 -0400 Processing by EventsController#create as HTML Parameters: { "utf8" = > "✓" , "authenticity_token" = > "zo5fvibHtpivjqseJ+AL4ToTTg8fUzSuXxuyvYzdfj6fUZfq7Y0x584Y/fXRSnWffvCLdaJV2J0vkp273GbB6Q==" , "event" = > { "name" = > "some name" , "address_attributes" = > { "address" = > "some address" }} , "commit" = > "Create Event" } ( 0.1ms ) begin transaction ( 0.0ms ) rollback transaction

Why can't our transaction complete? To find out, let's go back to the event controller and insert a byebug statement after an event save fails

1 2 3 4 5 6 7 8 9 10 11 12 13 def create @event = Event . new ( event_params ) respond_to do | format | if @event . save format . html { redirect_to @event , notice: 'Event was successfully created.' } format . json { render :show , status: :created , location: @event } else byebug #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ADD THIS format . html { render :new } format . json { render json: @event . errors , status: :unprocessable_entity } end end end

If an error occurred when saving an event (ie our transaction was rolled back), byebug will halt server execution and open the terminal to that line. Try to save an event again, and notice a prompt appears in the terminal:

1 2 3 4 5 6 7 8 9 10 11 12 [ 32, 41] in /~/polymorphic-rails/app/controllers/events_controller.rb 32: if @event.save 33: format.html { redirect_to @event, notice: 'Event was successfully created.' } 34: format.json { render :show, status: :created, location: @event } 35: else 36: byebug => 37: format.html { render :new } 38: format.json { render json: @event.errors, status: :unprocessable_entity } 39: end 40: end 41: end ( byebug )

We can use this prompt to find the issue. Typing in @event.errors yields

1 #<ActiveModel::Errors:0x007f54fa167258 @base=#<Event id: nil, name: "some name", created_at: nil, updated_at: nil>, @messages={:"address.addressable"=>["must exist"]}, @details={:"address.addressable"=>[{:error=>:blank}]}>

There's the issue - address.addressable"=>["must exist"] . Address.addressable is the reference to the parent table, an Event. Rails 5 recently made this association required by default, but that doesn't explain why the association is missing.

This seems to be a bug on Rails part. It turns out that if you disable the new validation address.addressable will be filled in, so there's some sort of issue with the ordering of events.

Regardless, let's get it working. You can turn off the Address model's ( app/models/address.rb ) validation check:

1 2 3 class Address < ApplicationRecord belongs_to :addressable , polymorphic: true , optional :true end

OR you can leave the validation check on and manually assign addressable in event#create ( app/controllers/events_controller.rb ). I originally went with this second option, but found it caused issues when seeding my database. Event cannot exist without an address, but address could not exist without an event (Addressable). Thus I ended up switching to the first option.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 def create @event = Event . new ( event_params ) @event . address . addressable = @event #<<<<<<<<<<< ADD THIS respond_to do | format | if @event . save #Saves the event. Addressable has something to reference now, but validation does not know this in time, thus it has to be done manually above 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

Let's check that it works. Insert a byebug into the event controller's show action. Now navigate to /events/new and submit a completed form. Byebug will halt execution as the form redirects to a 'show' page. Ensure that everything is playing together nicely:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 Started GET "/events/12" for 127.0.0.1 at 2017-07-13 15:04:40 -0400 Processing by EventsController#show as HTML Parameters: { "id" = > "12" } Event Load ( 0.2ms ) SELECT "events" . * FROM "events" WHERE "events" . "id" = ? LIMIT ? [[ "id" , 12], [ "LIMIT" , 1]] Return value is: nil [ 9, 18] in /~/polymorphic-rails/app/controllers/events_controller.rb 9: 10: # GET /events/1 11: # GET /events/1.json 12: def show 13: byebug => 14: end 15: 16: # GET /events/new 17: def new 18: @event = Event.new ( byebug ) @event #<Event id: 12, name: "asd", created_at: "2017-07-13 19:02:40", updated_at: "2017-07-13 19:02:40"> ( byebug ) @event.address Address Load ( 0.3ms ) SELECT "addresses" . * FROM "addresses" WHERE "addresses" . "addressable_id" = ? AND "addresses" . "addressable_type" = ? LIMIT ? [[ "addressable_id" , 12], [ "addressable_type" , "Event" ] , [ "LIMIT" , 1]] #<Address id: 8, address: "asd", addressable_type: "Event", addressable_id: 12, created_at: "2017-07-13 19:02:40", updated_at: "2017-07-13 19:02:40"> ( byebug ) @event.address.addressable Event Load ( 0.1ms ) SELECT "events" . * FROM "events" WHERE "events" . "id" = ? LIMIT ? [[ "id" , 12], [ "LIMIT" , 1]] #<Event id: 12, name: "asd", created_at: "2017-07-13 19:02:40", updated_at: "2017-07-13 19:02:40"> ( byebug )

Looks good!

So there you have it. You can check out the code for this project here. It's always frustrating when butting heads with issues in underlying tech, so hopefully this post helps make things easier. Thanks for checking out the guide and good luck!

PS:

If you want your address to geocode and do other fancy stuff check out geocoder.

Please enable JavaScript to view the comments powered by Disqus.