While building my mock, Central Perk, point of sale (POS) application, I had an idea. Is there a way to highlight the most popular menu item?
On a small scale, it would work like this:
- Get each order
- List the menu items from each order
- Count how many times each menu item is ordered
In the real world, manually counting items makes sense.
What about when model associations are involved?
Model associations can add complexity, but they also do a lot of the work for us. This post will outline how to work has many through relationships to find the most common item in your database.
Model Associations
Before we dive into making our most_popular method, let’s take a look at how our models are structured. In the Central Perk scenario, an order has_many menu items through order items.
This means that given a specific order, we can call and return the menu item objects (a collection) associated with that order.
For example:
order.menu_items
This would return the following collection:
#<ActiveRecord::Associations::CollectionProxy [#<MenuItem id: 2, name: "Caffè mocha", description: "Espresso, chocolate and steamed milk topped with c...", price: 2.5, created_at: "2019-12-15 04:09:49", updated_at: "2019-12-15 04:09:49">, #<MenuItem id: 4, name: "Matcha Green Tea Latte", description: "Matcha tea steeped in steamed milk and lightly swe...", price: 3.5, created_at: "2019-12-15 04:09:49", updated_at: "2019-12-23 02:29:01">]>
There’s a lot of information in my menu item model, so let’s simplify the data we will be working with.
Menu Items
ID | Name |
1 | “coffee” |
2 | “mocha” |
3 | “espresso” |
4 | “scone” |
5 | “puppuccino” |
Orders
ID | Menu Items |
1 | “coffee”, “scone” |
2 | “mocha”, “puppuchino” |
3 | “espresso”, “scone”, “puppuchino” |
4 | “mocha”, “puppuchino” |
5 | “coffee”, “puppuccino” |
Getting the Most Popular Menu Item
Get all menu items from all orders
The first thing we need to do is to collect all the menu items from all orders. Knowing that calling .menu_item
on one order returns the menu items for that order, we can iterate over all orders and add the menu items to an array.
all_items = []
Order.all.each do |order|
all_items << order.menu_items
end
Flatten the array and return it
Each time .menu_items
is called on an order, it returns an array. Those arrays are pushed into the all_items array, which ends up looking like this:
all_items = [["coffee","scone"],["mocha","puppuccino"],["espresso","scone","puppuccino"],["mocha","puppuccino"],["coffee","puppuccino"]]
To make the following steps easier, let’s flatten the array so that each item is in its own place within the array.
all_items.flatten
// ["coffee","scone","mocha","puppuccino","espresso","scone","puppuccino","mocha","puppuccino","coffee","puppuccino"]
Count the items
This is where a lot of the wow behind Ruby on Rails happens. At the end of this step, we’ll have a hash that looks something like this.
hash = {
“coffee”=>2,
“scone”=>2,
“mocha”=>2,
“puppuccino”=>4,
“espresso”=>1
}
The hash has a key for each menu item. The value for each key is the number of times that menu item appears in the all_items array.
Let’s break down how we get there.
First, we’ll call the .inject
method on the all_items array. The .inject method is a powerful, if not mysterious method. It accepts a block statement and two variables. The first variable is the memo, which is what holds all the items that will eventually pass through the block. The second is an element from the object that .inject is called on.
Here’s what it looks like.
all_items.inject(Hash.new(0)) {
|memo, item|
memo[item] += 1;
memo}
In our case, the memo is Hash.new(0)
, an empty hash where the default value for each key is 0. And an item is a menu item from the all_items array. As each item comes into the block, it is set as a key within the hash. Remember, adding a new key to a hash can be done like this:
hash[key] = value
And calling a value, by its key looks like this:
hash[:key]
// value
If a menu item (a key) does not already exist within the hash, it is created and 1 is added to the default value of 0. The next time that item/key is passed into the block, it value is reassigned to its current value plus 1. When all items have been injected in the block, the hash is returned. It’s tallying!
Let’s look at how the hash changes given the following list of items:
all_items = "coffee","puppuccino","puppuccino"
hash = Hash.new(0)
// When "coffee" is passed into the block
// { “coffee”=>1 }
// When "puppuccino" is first passed into the block
// { “coffee”=>1, “puppuccino”=>1, }
// When "puppuccino" is passed into the block a second time
// { “coffee”=>1, “puppuccino”=>2, }
Sort the hash
Since I am trying to find the most popular item on the menu, I want to put the hash values in order from highest to lowest. We can do that by calling the .sort_by
method on our hash.
hash.sort_by{|key,value| value}
Our hash, that originally looked like this:
{“coffee”=>2, “scone”=>2, “mocha”=>2, “puppuccino”=>4, “espresso”=>1}
Now looks like this.
[["espresso", 1], ["coffee", 2], ["scone", 2], ["mocha", 2], ["puppuccino", 4]]
Wait. Now it’s an array! And the values are in order from smallest to largest. How can we fix that?
To change the order, we can use the .reserve
method or change value
from within the .sort_by
block to -value
.
hash.sort_by{|key,value| -value
}// [["puppuccino", 4], ["coffee", 2], ["scone", 2], ["mocha", 2], ["espresso", 1]]
We’re getting close! The most popular item is right there in the nested array. A hash is a better format for data like this, so let’s add .to_h
to convert this array back into a hash. Our method now looks like this.
hash.sort_by{|key,value| -value}.to_h
Which gives us our sorted hash:
{
"puppuccino"=>4,
"coffee"=>2,
"scone"=>2,
"mocha"=>2,
"espresso"=>1
}
Getting the most popular item
We’re so close!
Within our sorted hash, the first key/value pair has the information we need. Let’s access it with the .first method.
most_popular_item = sorted_hash.first
// ["puppuccino", 4]
Cool! Puppuccino, appears four times, making it the most popular menu item in our sample of orders.
Putting it together
If we take everything we worked with and put it together, it looks like this within our Order model.
Now that we have the most popular item, there is a lot we can do with it. For example, we can:
puppuccino = MenuItem.find_by(name: most_popular_item[0])
// <MenuItem id: 5, name: "puppuccino", description: "Whipped cream in a small cup for your favorite pooch.">
Print out a statement:
puts "#{puppuccino.name}, is the most popular item at Central Perk!"
Or, tagging it in our interface to bring attention to it.

We can even update our most_popular
method to return the top three most popular items. The possibilities are endless.
One response to “Using .inject to Find the Most Popular Item in a Has_Many, Through Relationship”
[…] Using .inject to Find the Most Popular Item in a Has_Many, Through Relationship […]