It Was All Going So Well...Then a Churro Ruined Everything
Note: This is going to be a technical article about Ruby on Rails model relationships and how the iconic Disneyland churro made us rethink our entire design.
The Friendliest.app was designed around a straight-forward set of Rails model associations:
- A Resort has many Parks
- A Park has many Areas
- An Area has many Restaurants
- A Restaurant has many Meals
So, you could have a Dole Whip (Meal), served at The Tropical Hideaway (Restaurant), in Adventureland (Area), in Disneyland (Park) at the Disneyland Resort (Resort).
It was all working perfectly. We built out the Resorts, Parks and Areas, indexed the first 40 restaurants and 70-ish allergy-friendly meals. Smooth sailing, no issues.
And then we got to the churro...and that simple, delicious stick of fried dough stopped us dead in our tracks...
Well, the churro was the first item we ran into that is available, under the same name, at several different restaurants, across different areas, in different parks.
And why does that matter?
With our structure, a meal must belong to a single restaurant. If 8 Restaurants sell churros, we would need to create 8 churro Meals, one for each location. On the surface, this isn't a big deal, but it creates several design challenges:
- When churro information changes, each churro Meal needs to be updated.
- When you search for 'churro', you receive many results that appear identical.
- Favorites and comments would exist at the individual meal level and not be cohesive.
The quick and dirty fix would be to change the model association from a Restaurant has many Meals to a Restaurant has and belongs to many Meals and a Meal has and belongs to many Restaurants.
Before you scream, don't worry, this in not the direction we went. There are very few case where you should use a has and belongs to many association.
Another option would be to leave our Meals model as-is and create a second "CommonMeals" model to handle items like churros. We dismissed this one quickly after looking at the additional logic and queries that would be required to maintain two separate models, but stitch them together throughout our views.
We opted to leverage the has many through association. To do this we created a join table, CommonMeals and updated our relationships to looks like:
Using CommonMeals, we now have an extendable join table to support the many-to-many relationship between Restaurants and Meals. This future-proofs us a bit for other potential scenarios like an item with different price points at different locations or an item that is bundled with a side at some locations and sold on its own at others. Both of these can be easily handled by extending the CommonMeals table to support these new facets of the Restaurant/Meal relationship.
This did create some new challenges with existing app functionality. For example, when adding a Meal to a List, you might want to specify a Restaurant for the Meal. With the original one-to-many association this wasn't an issue since each Meal belonged to a single Restaurant.
Now, if a Meal has many Restaurants through CommonMeals, we need to ask which Restaurant to assign before saving the Meal to a List.
The last piece of the puzzle was extending our ListIems model to include Restaurant IDs. After that is was a matter of updating all of our Views that depended on the old one-to-many relationship and creating (and testing) queries to initially populate the join table and ListItem Restaurant IDs.
All in all, it took about a week, from initially settling on a solution to triggering the queries to migrate our production data to the new relationships...all because of a churro.
Kevin Williams | Tuesday, May 11th 2021