Working with associations was the most difficult part of building Central Perk, my Ruby on Rails application.
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 app and here’s how I solved them.
For my app, I planned on three models. Users (baristas), orders and menu items (products). The relationship would like this:
With those models, programmatically, that I wanted to be able to do the following:
- Create a new order and automatically associate it with a user.
- Be able to see all orders that belong to a user.
- See all menu_items (products) in a specific order.
Everything with setting up my models seemed to be working fine up until the last part. Currently, an Order with an attribute of
:menu_items could only have one value. Meaning, just 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? This is where a
has_many through relationship came in.
I knew that
order.menu_items needed to be an array. To solve for this, I needed a fourth model, OrderItem.
OrderItem would be a join table, with foreign keys to the Order and MenuItems models. I thought 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 products 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?
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:
Visually, I thought of the relationships between the four models as looking like this:
This was making sense! I would not need to reference OrderItems directly. Active Record does that work for me. Since an Order has many OrderItems, referencing just the Order would give me access to MenuItems. My updated models now looked like this:
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 Order was not saving 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.
order.menu_items # [1,2,3]
- Add an item to an order.
order.menu_items << item
- Save the order.
Then it hit me.
Ruby was right in not permitting the
menu_item_ids param. I thought I just 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 params) and shovel them into the array. I updated my create order method.
And it worked!
In summary, if you are running into issues with objects 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 hash, the transaction will fail (and not write to your database, yay!) if an attribute of :not_a_hacker was within your params.
create!will give more information into what validations may be causing errors. For example, in my app, an Order must have as User (barista) 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.
.valid?to objects. A object may be updating. But, is it saving properly to the database? For example, if
Order.create()(an empty object, which will not validate because there is no
.valid?will return false.
All in all, building Central Perk was the most difficult and frustrating project to date. However, I learned a lot about associations, forms, params and creating and overall DRY application. I am more confident in my debugging skills and look forward to improving what I have built so far.