March 22, 2016 by Daniel P. Clark

Rails: Has One Through Polymorphic Relation

As I’ve described in my blog post “Rails Polymorphic Associations” I’ve stated that polymorphic relations in Rails are best for scenarios where a model may “belong to” one of many other kinds of model.  So a Comment model with the polymorphic commentable relationship can belong to any other record in your Rails app.  This is a child to be adopted by any of one many parents kind of relationship.

But recently I wanted to do the inverse of this and say that an Item model can be one of any Product.  An Item is something you can keep in a Location and a Product really can be anything.  So I did some digging and found a few short examples online suggesting using a middle-man model with to belongs_to relations with one being polymorphic to allow the polymorphic child type of relation.  This is a Item has_many polymorphic Products through MiddleMan kind of thing.  So I went ahead and tried it out and have been very successful in it’s design.  So now I’m here to share with you how to do it as well. This will be more “reading source code” than commentary.  You’re more than welcome to ask questions.

The Models

I’ll briefly show how I set up the models.

rails g model Location name:string:index
# then modified the migration for the following
# t.string :name, unique: true, null: false

rails g model Item placeable:references{polymorphic}

rails g model ItemProduct item_id:integer:index product:references{polymorphic}
# then modified the migration for the following
# t.integer :item_id, unique: true, null: false

rails g model Book isbn:text:index title:text:index author:text:index publisher:text:index year:integer:index

rails g model Movie title:text:index director:text:index writer:text:index year:integer:index length:text:index
# then modified the migration for the following
# t.text :title, null: false

I choose to use the ‘text’ type rather than ‘string’ because I’m using a PostgreSQL database and they’re optimized it in such a way where the ‘text’ type is as performant as ‘string’ and even has extra features you can take advantage of.  I chose to give Item a polymorphic placeable reference for it’s location as locations will be marked shelves but items can be placed in containers on shelves and those can be labelled as a place which stores items.

So now we run rake db:create && rake db:migrate and all of our models are generated in the database.  Next we need to edit the model relationships in app/models to infer what relationships these models have to each other.  Here’s how I have them.  Don’t mind the full model files here, I thought it would be nice to share everything I added to them.

# app/models/location.rb
class Location < ActiveRecord::Base
  has_many :items, as: :placeable
  validates_presence_of :name
  validates :name, format: {
    with: /\A[A-Z0-9]+\z/,
    message: "only allow uppercase alphanumeric labels"
  }
  default_scope { order(name: :ASC) }
end

# app/models/item.rb
class Item < ActiveRecord::Base
  belongs_to :placeable, polymorphic: true
  validates_presence_of :placeable_id
  has_one :item_product, dependent: :destroy
  has_one :book, through: :item_product, source: :product, source_type: 'Book'
  has_one :movie, through: :item_product, source: :product, source_type: 'Movie'
  accepts_nested_attributes_for :item_product, allow_destroy: true

  def self.kinds
    [Book, Movie]
  end

  def location
    placeable
  end

  def product
    item_product&.product
  end

  def product_name
    product&.class&.name&.downcase
  end
end

# app/models/item_product.rb
class ItemProduct < ActiveRecord::Base
  belongs_to :item
  belongs_to :product, polymorphic: true, dependent: :destroy

  def place
    item.placeable
  end
end

# app/models/book.rb
class Book < ActiveRecord::Base
  has_one :item_product, as: :product
  has_one :item, through: :item_product
  include Locatable # def location; item_product.item.placeable end
  include Referable # def ref; item_product.item end
end

# app/models/movie.rb
class Movie < ActiveRecord::Base
  has_one :item_product, as: :product
  has_one :item, through: :item_product
  include Locatable # def location; item_product.item.placeable end
  include Referable # def ref; item_product.item end
end

With the design being for a polymorphic child type of relationship I also wanted the page views to be polymorphic in design.

Controllers/Views

When I generated my structure I actually used rails generate resource rather than scaffolding.  And to my surprise I found my controllers bare with no methods.  I found this an interesting experience as it let me feel my way through and see how I might do things differently.

app/controllers/location_controller.rb

class LocationsController < ApplicationController
  before_action :set_location, only: [:edit, :show]
  def index
    @locations = Location.all
  end

  def new
    @location = Location.new
  end

  def create
    begin
      if location_params[:name][/\.\./]
        Range.new(*location_params[:name].split("..")).to_a
      else
        location_params[:name].del(" ").split(',')
      end
    end.each do |n|
      @location = Location.new(name: n)
      break unless @location.save
    end
       
    if @location.persisted?
      redirect_to "/" 
    else
      render 'new'
    end
  end

  def show
  end
  
  private
  def location_params
    params.require(:location).permit(:id, :name)
  end 

  def set_location
    @location = Location.where(id: params[:id].sift("0".."9")).first
  end
end

You can see I got a bit creative with the create method.  I decided I wanted to be able to create locations in batches so I can save time.  The String#sift method in set_location, and the String#del method in create, are both from my gem MightyString and they allow you to filter your strings easily.

app/views/locations/index.html.erb

<% flash.each do |name, msg| -%>
  <%= content_tag :div, msg, class: name %>
<% end -%>
    
<div class="locations-main">
  <h3>Locations [<%= link_to "New", new_location_path %>]</h3>
  <%= link_to "Items", items_path %>
  <ul>
  <% @locations.each do |location| %>
    <li>
      <%= link_to location.name, location_path(location) %>
    </li>
  <% end %>
  </ul>   
</div>

In building this project I really wanted to go with a bare bones functional design.  I believe that’s how all projects should start.  Getting into styling things too much early on just causes more work later.  Here’s a bit of style for this page:

.locations-main ul li {
  font-family: helvetica;
  font-size: 20px;
  float: left;
  margin-right: 12px;
  display: inline;
}

I find that using HTML lists permits a lot more flexibility with CSS style rather than using tables.

And here’s the new location page:
app/views/locations/new.html.erb

Accepts three different kinds of input.
<ol>
  <li>Single location: <strong>A1</strong></li>
  <li>Comma separated locations: <strong>A1, A2, A3</strong></li>
  <li>Range of locations: <strong>A1..A9</strong></li>
</ol>

<%= form_for :location, url: locations_path do |f| %>

  <% if @location.errors.any? %>
    <div id="error_explanation">
      <h2>
        <%= pluralize(@location.errors.count, "error") %> prohibited
        this article from being saved:
      </h2>
      <ul>
        <% @location.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <p>
    <%= f.label :Location %><br>
    <%= f.text_field :name %>
  </p>

  <p>
    <%= f.submit %>
  </p>

<% end %>

<%= link_to 'Back', locations_path %>

And that’s it for locations.  It’s a very simple implementation.  I don’t have any deletion or edit feature in it.  I don’t need it right now.  If I make a mistake I just open up the Rails console and look it up. Location.where(name: “A55”).first.update(name: “A5”)

Now comes the more complex controller/view…

Polymorphic Item Products

I’ll go over controller details before discussing the view on this one.

app/controllers/item_controller.rb

class ItemsController < ApplicationController
  def index
    @items = if Item.kinds.map {|k|k.to_s.downcase}.include? params[:kind]
               Item.joins(params[:kind].to_sym)
             else
               Item.all
             end.includes(:placeable).sort_by {|i| i.placeable.name }
  end

  def new
    @item = Item.new(placeable_id: item_params[:placeable_id])
    @item.build_item_product( product: item_kind.new )
  end

  def create
    @item = Item.new(
      placeable_id: item_params[:placeable_id],
      placeable_type: item_params[:placeable_type] || 'Location'
    )
    @item.build_item_product(
      product: item_kind.new( item_params[:item_product_attributes] )
    ) 
    if @item.save
      redirect_to @item
    else
      flash[:error] = @item.errors.first
      redirect_to "/"
    end
  end

  def show
    @item = Item.find(params[:id]).item_product.product
  end

  def update
    item = Item.find(params[:id])
    item.update!(item_params)
    redirect_to item 
  end

  def destroy
    item = Item.find(params[:id])
    item.destroy
    redirect_to "/"
  end

  private
  def item_params
    params.require(:item).permit(:kind, :placeable_id, :placeable_type, item_product_attributes: ipa)
  end

  def ipa
    item_kind&.column_defaults&.keys.try(:-,[:id,:created_at,:updated_at])
  end

  def item_kind
    Item.kinds.detect {|k| k.name == params.dig(:item,:kind) }
  end
end

The index checks the Item.kinds method to see if the parameter for a kind matches the predefined acceptable models for Item.  If it does match the results will be filtered to only the kind given (e.g. Books or Movies).  This allows the index page to switch the results of it’s content based on kinds, or everything.  I found that using includes(:placeable) brought the query down from one for every item to just two simple queries to the DB.

The new method controller first creates a new Item with the location provided in the parameters.  It then builds a ItemProduct with a new “product” created under the parameter {:product => thing} .  The item_kind is a private method at the end of the controller which picks a valid model from the Item.kinds method and creates a new instance of it with item_kind.new .  Seeing the use of the polymorphic attribute :product reference an object directly was something new to me build_item_product( product: item_kind.new ).  It’s cool that polymorphic references give us that much flexibility.

The create method here is pretty much doing the same thing as new.  Except that is has all the attributes and performs a save action.

I’ve implemented the show method a bit deceptively.  I’ve kept the @item instance variable when the object being return is the product (a book or movie).  You’ll just have to keep this in mind when we get to the view.

The item_params needed to be polymorphic in whichever model it supported nested attributes for, so for that I have the ipa method get all of the database column names for the current product and remove [:id, :created_at, :updated_at] .  Congratulations!  You now have polymorphic nested attributes permitted.

Now the views

app/views/items/index.html.erb

<h3><%= link_to "<", "/" %>&nbsp;Items</h3>
<div id="items-menu">
  <ul>
    <% Item.kinds.each do |kind| %>
      <li><%= link_to kind.to_s.pluralize, items_path(kind: kind.to_s.downcase) %></li>
    <% end %>
  </ul>
</div>
<hr/>     
<table id="location-catalog" border="2">
  <% @items.each do |item| %>
    <tr>
      <td>L:&nbsp;<%= item.location&.name %></td>
      <td><%= link_to item.product.class.name, item_path(item) %></td>
      <% Hash(item.product&.attributes).each do |c, v| next if ["id", "created_at", "updated_at"].include?(c) %>
        <td><table border="0"> 
          <tr>
            <td><strong><%= c.humanize %></strong>:</td>
            <td><%= v %></td>
          </tr>
        </table></td>
      <% end %>
    </tr>
  <% end %>
</table>

This view is rather simple in structure.  The first link is simply a link to home with a less than symbol < .  The next links are to each of the kinds of product item supports.  It links to the items_path with a parameter of :kind which will show up in the address bar’s url to filter the pages contents.  And in the table I render out a row for every kind of product by getting their attributes in hash form.  I use the c variable to represent column and v to represent value.  And that’s how the index works.

Now for the polymorphic “new” product page
app/views/items/new.html.erb

<h3>New <%= @item.product_name %></h3>                                                                

<%= form_for @item do |f| %>
  <%= f.hidden_field :placeable_id, value: @item.placeable_id %>
  <%= f.hidden_field :kind, value: @item.item_product.product_type %>
  <%= render partial: "partials/#{@item.product_name}_fields", locals: {f: f} %>
  <%= f.submit %>
<% end %>

The important part here is rendering the partial based on what the product name is.  @item.product_name is a method in the Item model from earlier.  It simply converts the model name to a string and down-cases it.  So if the item is a book this page will fill in the rest of the form from the partial view at app/views/partials/_book_fields.html.erb and pass the form variable f into the partial’s scope.  For each of the models I do create their own unique form fields and I keep it simple.  Here’s the one for Book:

app/views/partials/_book_fields.html.erb

<ol>
<%= f.fields_for :item_product, @item.product do |ip| %>
  <li>ISBN:      <%= ip.text_field :isbn %></li>
  <li>TITLE:     <%= ip.text_field :title %></li>
  <li>AUTHOR:    <%= ip.text_field :author %></li>
  <li>PUBLISHER  <%= ip.text_field :publisher %></li>
  <li>YEAR:      <%= ip.text_field :year %></li>
<% end %>
</ol>

It’s the same design for all other model partials.

The show path

app/views/items/show.html.erb

<h3><%= link_to "<", location_path(@item.location.id) %> <%= @item.class.name %></h3>
<table>
  <tr>
    <td>
      <strong>Location:</strong>
    </td>
    <td><strong>
        <%= dynaspan_select(@item.ref, :placeable_id, {
          choices: options_for_select(@item.location.class.pluck(:name,:id)),
        }) %>
    </strong></td>
  </tr>
</table>
<ol>
  <% @item.attributes.each do |c,v| %>
    <li><strong><%= c.humanize %></strong>: <%= v %></li>
  <% end %>
</ol>
<%= link_to :Remove, item_path(@item.ref), method: :delete, data: { confirm: "Are you sure?" } %>

Still keeping it simple.  The item attributes are shown polymorphically with the hash and the c, v local variables representing column and value.  One thing might jump out to you is the dynaspan_select method.  This is a gem (DynaSpan) I’ve written that lets you “magically” click on text to change to an input field where all changes get submitted to the server without the page navigating away by submitting record updates via AJAX and then the input field vanishes like Houdini to the text of the new value.  This method is just my way of simplifying something awesome and really sticking to the motto of “Keep code beautiful.  There’s beauty in simplicity.”  The use of dynaspan_select here allows the user to click the location, get a drop down menu, and instantly change the items location. 🙂

app/views/locations/show.html.erb

<h3><%= link_to "<", "/" %> Location: <%= @location.name %></h3>

<%= form_tag(new_item_path, method: :get) do %>
<%= hidden_field_tag 'item[placeable_id]', @location.id %>
New Item: <%= select_tag 'item[kind]', options_for_select(Item.kinds.map(&:to_s)) %>
<%= submit_tag("Create") %>
<% end %>
<hr/>
<table id="location-catalog" border="2">
  <% @location.items.each do |item| %>
    <tr>
      <td><%= link_to item.product.class.name, item_path(item) %></td>
      <% Hash(item.product&.attributes).each do |c, v| next if ["id", "created_at", "updated_at"].include?(c) %>
        <td><table border="0">
          <tr>
            <td><strong><%= c.humanize %></strong>:</td>
            <td><%= v %></td>
          </tr>
        </table></td>
      <% end %>
    </tr>
  <% end %>
</table>

This view has the only form you will need to select creating a new product as it will set which kind of item in the /items/new resource.

And that’s it!

This was a minimalist project for me to inventory my own belongings.  It was enjoyable to do, educational, and helps me put all my warehouse experience to work in organizing.  The website experience is seamless, simple, and easy on the eyes.  I did not expect learning the use of a has_X-through scenario would work out so easily for me.  But overall this was a great experience for me.  I’m sure there are plenty of questions to ask about the code here so feel free to jump in!

After this went so well I thought I’d give a shot at converting an existing polymorphic relation with many has_one-throughs already with an extra middleman in it in a big project.  Keep in mind that all the pages for the model are already built.  Let me say it was not seamless.  I put about 4 hours into it and got the 1st level of relations through it working (e.g. Contact through ProfileReference to Profile).  But I didn’t get the Profile’s nested relations working and I already hacked through a lot of code.  I ended up throwing all that work away by git stashing it and deleting the stash.  Still a good learning experience.

As always I hope you’ve enjoyed this!  Please comment, share, subscribe to my RSS Feed, and follow me on twitter @6ftdan!

God Bless!
-Daniel P. Clark

Image by Thomas Hawk via the Creative Commons Attribution-NonCommercial 2.0 Generic License.

#controller#has_on#inventory#model#nested#parameters#params#polymorphic#rails#through#view

Comments

  1. makabde
    April 3, 2016 - 11:04 am

    This is exactly what I needed. I am currently building an API in rails and could not find a proper way of achieving this type of association.
    I am wondering how you would do, so that the `show` method in the ItemsController returns not only a `Product` attributes but also the attributes of the `Item`.
    Thanks!

    • Daniel P. Clark
      April 3, 2016 - 4:08 pm

      In that case I would change the show method in the controller to

      
      def show
        @item = Item.find(params[:id])
        end
      

      And in the view simply treat @item for the item attributes @item.name, @item.description, etc. And for the product details along with it you could use the `product` method in the Item model which points to `item_product.product` and get the attributes along with the @item: eg) @item.product.keywords, @item.product.location, etc.

      Assuming something may go wrong and you don’t want it to break when product is nil (this should never happen you should want it to break). You can write the view in such a way that it safely handles nil.

      
      
        
        
        
          
          
        
      
      

      This is typically good design for has_many situations but you can use it for has_one.

  2. Stephen Murdoch
    January 17, 2017 - 3:56 am

    Excellent writeup, I have a question though:

    When I visit ‘/items/new’ I get “undefined method `new’ for nil:NilClass”

    I know that this is caused by this line:

    `@item.build_item_product( product: item_kind.new )`

    This is happening because params[‘item’][‘kind’] (called by the **item_kind** method) is nil when you GET the :new action.

    I know I can make it work by visiting /items/new?[item][kind]=Book but I don’t like that.

    What technique are you using to ensure that item_kind returns something other than nil when visiting /items/new?

    Cheers

    • Daniel P. Clark
      January 17, 2017 - 8:25 am

      There are three ways I might go about doing this. And in my opinion

      nil

      can be very useful here.

       
      # if you don't mind passing nil as a product option then you just need to safely call new
      @item.build_item_product( product: item_kind.try(:new) )
      
      # Since Ruby 2.3 you can use the lonely man operator for this
      @item.build_item_product( product: item_kind&.new )
      
      #An lastly if you want to only build if a kind is selected then add a conditional at the end
      @item.build_item_product( product: item_kind.new ) if item_kind
      

      If you want to have a default if no kind is given you can do

      
      @item.build_item_product( product: item_kind.try(:new) || Book.new )
      # or
      @item.build_item_product( product: item_kind&.new || Book.new )
      
      • Daniel P. Clark
        January 17, 2017 - 8:42 am

        Looking at my code again I realized that item_kind should not be returning nil for you. This resource should only be loaded when a form has sent the proper parameters. If you are getting nil I would rewrite it for a redirect.

        
        def new
          @item = Item.new(placeable_id: item_params[:placeable_id])
          if item_kind
            @item.build_item_product( product: item_kind.new )
          else
            flash[:error] = "Invalid item kind provided!"
            redirect_to request.referer
          end
        end
        

        This site wasn’t designed for visiting /items/new directly.

    • Daniel P. Clark
      January 17, 2017 - 9:01 am

      That was my mistake I forgot to post the locations view. I’ve updated the post and added it to the end. You should never visit /items/new directly (it wasn’t designed that way).

      • Stephen Murdoch
        January 17, 2017 - 9:01 pm

        Thanks! Everything works perfectly. Awesome article.

Leave a Reply

Your email address will not be published / Required fields are marked *