πŸš€ See the 2024 Ruby on Rails Community Survey results!
Article  |  Development

Ranked_Model: A Rails Gem for Multiple Data Sets

Reading time: ~ 3 minutes

Ranked_Model: A Rails Gem for Multiple Data Sets

Recently we ran into an issue on a client Rails application when we wanted to customize the order in which certain items would appear on various pages. The admin users would log in to a CMS built with active_admin, create a widget with information about a company, and then either manually set the position in the list while creating or editing the widget, or use a 'move up and down' feature on the index page.

Originally, we utilized a ranking gem, acts_as_list to implement this feature as it allowed us to quickly and painlessly add all the functionality we needed.

I ran into an issue, though, when our client requested that each widget have two separate rankings, depending on which page the user was visiting. If the user was on the Foo product page, the widget might appear at position 2, but on the Bar page the widget would render in position 3.

My initial instinct was to create a new position attribute on the widget model and pass in both the new attribute and the existing position column into the acts_as_list method. However, I hit a roadblock since acts_as_list is not capable of handling two different attributes on a single model.

This was a time sensitive request and as I dove into the possible solutions of using associations and join tables to add this feature with acts_as_list, I couldn't help but feel that there should be a quicker solution that could handle having two different ranking systems for a single model.

My search led me to discover ranked_model, a ranking gem with a similar goal to acts_as_list but with a different implementation and functionality, most of which fit our criteria with much more ease than the acts_as_list approach.

After installing the gem and including RankedModel in the class file, you can simply declare which attributes will be handled by ranked_model with the following lines:

ranks :foo ranks :bar

This would work fine on a greenfield project or a new attribute but I was adding this to an attribute that already had position-oriented ranking applied. Both gems use integers as their data types, but for the most part the similarities end there.

Acts_as_list uses sequential numbers in its assigned attribute, incrementing and decrementing multiple objects as necessary with each change in the order. For example by taking a list of widgets with positions of A: 1, B: 2, C: 3, & D: 4, then moving D to the first position, D would become 1 but B, C, and A would all be incremented to fully reorder the list. There are more methods to run each time but it’s simple to glance at any given object and understand its exact position.

On the other hand,ranked_model utilizes relatively large numbers, both positive and negative, to more quickly and efficiently order objects. Creating the same 4 objects with ranked_model looks far different than acts_as_list, being more like this:

A: -2342567, B: -1437689, C: 1289078, D: 2165876

Since ranked_model generally deals with larger integers, it can cause more difficulty in development and debugging, particularly in the console. Once a sizable number of objects is reached, finding the relative place in the ranking from only its position attribute would require notable attention to detail. However, calling a method to actually move these positions is simple enough.

To mimic the same widget repositioning as the acts_as_list example, moving D to the first position, the code would require a call of:

D.update_attribute :foo_ranking_position, :first

which would result in something like:

D: -2465876, A: -2342567, B: -1437689, C: 1289078

So you can see, using these large integers allowed us to reorder D to the front and only modify a single object! This becomes especially valuable in situations with large datasets. As mentioned earlier, ranked_model also allows us to call rankings on multiple attributes, so using:

D.update_attributes {foo_ranking_position: :first, bar_ranking_position: :last}

would be completely acceptable.

Migrating from acts_as_list to ranked_model did take a little bit of work, though. Since we already had an ordered attribute from acts_as_list, attempting to just write in and start using ranked_model on it outright led to some strange interactions and incorrect sorting. My solution was to add the following to a database migration:

  reversible do |direction|
    direction.up do
      Widget.order(:foo_position).each do |widget|
        widget.update_attribute :foo_position, :last
      end
      Widget.rank(:foo_position).each do |widget|
        widget.update_attribute :bar_position, :last
      end
    end
  end

This is probably a little overkill and almost certainly could be refactored to look more elegant or efficient (and also should probably be a rake task) but it worked with our need for a quick turnaround. It's essentially going through our original order from acts_as_list and telling ranked_model to order it at the end of the list, making sure all positions now have a ranked_model compatible position.

After using some of the syntax particular to the Gem, in order to modify the methods and controllers that were updating position in the class file, ranked_model proved to be an efficient way to sort and rank multiple datasets for Rails.

Have a project that needs help?