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.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:
shannon.orders.create
shannon.orders
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.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.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
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.
order = Order.create(user_id: 1, name_for_pickup: "Rachel", menu_item_ids: [1,2,3])
order.menu_items // [1,2,3]
order.menu_items << item
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
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.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.
Photo by Annie Spratt on Unsplash
Previously published at https://shannoncrabill.com/blog/ruby-on-rails-debugging-model-associations/