7807604376_d905ca8637_z

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.

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.

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

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

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:

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

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

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

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

The important part here is rendering the partial based on what the product name is. [email protected]_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

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

The show path

app/views/items/show.html.erb

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 “keeping it simple”.  The use of dynaspan_select here allows the user to click the location, get a drop down menu, and instantly change the items location. :-)

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