Debugging Has_Many, Through Relationships in Ruby on Rails

Written by scrabill | Published 2020/02/02
Tech Story Tags: ruby-on-rails | active-record | debugging | ruby | programming | database | software-development | tutorial

TLDR A mock Central Perk, Ruby on Ruby on Rails application, has many-to-many relationships. The relationship between Users (baristas), Orders and Menu Items (products) is where it got weird. An Order with an attribute of 'Many to Many' could only have one value per Order. An order, which belongs to a user, could have many Menu Items. An order could have more than one Menu Item, per Order. An Order would essentially be a collection of all OrderRecords with the same order_id. But, through other associations, it extends the functionality of those models.via the TL;DR App

Working with model associations can be difficult.
While building my mock Central Perk, Ruby on Rails application.
has_many
and
belongs_to
relationships made sense, but the addition to many-to-many—
has_many through
—relationships are where it got weird.

I ran into several issues while building my application and this is how I solved them.

Model Behavior

For my project, I planned on having three models: Users (baristas), Orders and Menu Items (products). A user could have many orders. An order, which belongs to a user, could have many Menu Items
With those models, programmatically, I wanted to be able to do the following:
  • Create a new Order and automatically associate it with a User.
    shannon.orders.create
  • Be able to see all orders that belong to a User.
    shannon.orders
  • See all MenuItems (products) in a specific order.
    order.menu_items
After setting up my models, everything seemed to be working up until
order.menu_items
. Currently, an Order with an attribute of
menu_items
could only have one value. Meaning, one MenuItem per Order. Can you imagine if you had to get back in line and start a new order for each item you wanted to buy at a coffee shop?
Ross and Rachel looking confused
This is where a
has many through
relationship came in.

Many to Many

If I wanted more than one MenuItem, per Order, the result of
order.menu_items
needed to be an array. This is where a four,
OrderItem
model comes in.
OrderItem
acts as a join table, with foreign keys to the
Order
and
MenuItems
models. In this example, think of each OrderItem record has a transaction instance, representing one
Order
and one
MenuItem
at a time.
An Order would essentially be a collection of all
OrderItem
records with the same
order_id
. I was a step closer to figuring out what I needed.

But?

At first, an
OrderItem
model made sense.
Until, it didn’t.
Would I need to call
order.order_items.menu_items
to see all the items in that order? My app had a
User
model too. How do you build a has_many through a relationship when there are more than three models?
Pheobe running around screaming "What do I do?"
In reality, has_many through only works with three models. But, through other associations, it extends the functionality of those models. If I wanted to know how many MenuItems were in the first order, created by a specific user I could call something like this:
user.orders.first.menu_items.count
.
Visually, I thought of the relationships between the four models as looking like this:
Visualization of the has_many through relationship of User, Order (which contains OrderItems) and Menu Items
This was making sense!
I would not need to reference
OrderItems
directly. ActiveRecord does that work for me. Since an
Order
has many
OrderItems
, referencing the
Order
would gives me direct access to
MenuItems
.
My finalized models now looked like this:
class User < ApplicationRecord
  has_secure_password
  has_many :orders
end

class Order < ApplicationRecord
  belongs_to :user
  has_many :order_items
  has_many :menu_items, through: :order_items
end

class OrderItem < ApplicationRecord
  belongs_to :order
  belongs_to :menu_item
end

class MenuItem < ApplicationRecord
  has_many :order_items
  has_many :orders, through: :order_items
end

Params

With the associations complete, I needed a form to create the
Order
object. At first, everything seemed to be working. But after looking closer at the console, I realized the transaction was getting rolled back and orders were not being saved to the database.
I noticed the
:menu_items_id
key was listed in my strong params, but I was getting a "
:menu_items_ids
is not permitted error".
To try and resolve this, I worked in the console, testing things out, bit by bit until I could pinpoint where I was getting stuck. In the console, I could successfully do the following.
  • Create an order.
    order = Order.create(user_id: 1, name_for_pickup: "Rachel", menu_item_ids: [1,2,3])
  • View the value of menu_items on an order.
    order.menu_items // [1,2,3]
  • Add an item to an order.
    order.menu_items << item
  • Save the order.
    order.save
Then it hit me.
Rails was right in not permitting the
menu_item_ids
param. I thought I needed to create an order. Instead, I needed to create an order,
find the menu items by id (menu_items_id, which was the unpermitted param) and shovel them into the array
.
I updated my create order method from this:
def create
  @order = Order.create(order_params)
  if @order.save
    redirect_to order_path(@order)
  else
    render :new
  end
end
To this
def create
  @order = Order.create(order_params)

  items_to_add = params[:order][:menu_item_ids]

  items_to_add.each do |item_id|
    if item_id != ""
      item_id = item_id.to_i
      item_to_add = MenuItem.find_by_id(item_id)
      @order.menu_items << item_to_add
    end
  end

  if @order.save
    redirect_to order_path(@order)
  else
    render :new
  end

end
And it worked!
Chandler jumping on a coffee table and doing a happy dance

Lessons Learned

In summary, if you are running into issues with object relationships, try the following:
Verify that the params are correct
Typos can instantly cause object creation to fail. Pluralization like
menu_item_id
vs
menu_item_ids
are also something to look out for. All params are strings, which may cause downstream effects if you are expecting an integer or boolean.
All model attributes are listed in strong params
Strong params help to prevent users from injecting harmful data into your database via the form. If strong params lists only
:name
,
:email
and
:password
can be submitted in a
User
model, the transaction will fail (and not write to your database, yay!) if an attribute of
:not_a_hacker
was within your params.
Use .create! when testing
create!
will give more information into what validations may be causing errors. For example, in my app, an
Order
must have as
User
(barista) user_id associated with it. Running
Order.create()
in the console would not tell me much, but running
Order.create!()
would print out an error like A User must exist.
Append `.valid?` to objects
An object may be updating, but, is it saving properly to the database? For example, if
order = Order.create()
(an empty object, which will not validate because there is no
:user_id
), adding
.valid?
will return false.

Summary

Model associations can be difficult and frustrating, but not impossible to work with. With some careful debugging, you can be a master of model associations in no time.

Resources

Previously published at https://shannoncrabill.com/blog/ruby-on-rails-debugging-model-associations/

Written by scrabill | Front End Software Engineer
Published by HackerNoon on 2020/02/02