For my most recent project I decided to build a simple app where one can keep track of bicycle maintenance. For example, a user might have a few different bikes that require a variety of maintenance at different intervals. The idea came while I was fixing both of my uncle's bikes- he is very bad at maintaining both of his bikes and he rides quite a lot, which unfortunately leads to prematurely worn components and more expensive repairs.
The functionality I wanted to build in would allow a user to create a profile for each bike, storing a name, description, and a serial number for identifying characteristics. Easy-peasy.
create_table "bikes", force: :cascade do |t| t.string "name" t.string "description" t.string "serial_number" end
The next major component to this app is the maintenance records themselves. I wanted to be able to track date, cost, and any notes or a brief description of the work performed... For example, if I wanted to remember the date that I had last changed the oil on my suspension fork, I wanted to be able to look for a record named "suspension tune" and retrieve the date the service was performed as well as a brief note- perhaps to state if you just changed the oil or to let yourself know that you had bled the damper or replaced the wiper seals in addition to the oil change. This is the basic schema I came up with:
create_table "maintenance_records", force: :cascade do |t| t.string "name" t.string "date" t.integer "cost" t.string "notes" end
Now that we have the bike schema and the maintenance record schema set up, I needed to include some basic user information. All I wanted here was really basic functionality, essentially the bare minimum to create a log-in feature so you can password-protect your data...
create_table "users", force: :cascade do |t| t.string "name" t.string "email" t.string "password_digest" end
Now, onto the relationships!
I wanted to associate the bikes to each user. Using ActiveRecord's awesome built in macros, this was a cinch! All I would have to do was add the has_many :bikes macro to my user model and the belongs_to :user macro in to my bike model, and run a couple of easy migrations to add a user foreign key to my bikes schema.
class User < ActiveRecord::Base has_many :bikes end
class Bike < ActiveRecord::Base belongs_to :user end
Moving forward to the maintenance records... This one was a challenge for me, an opportunity to confront what had been a difficult concept for me to understand head on. Since it was such a crucial piece of my original concept, I refused to compromise and forced myself to experiment with the nuances of relationships.
I knew that I wanted to be able to look up maintenance records that were associated with a particular bike, so the first thing I tried was setting up a belongs to/has many relationship between bikes and maintenance records. Similar to above, I just put the appropriate macros in each model- maintenance records belong_to :bikes, and bikes has_many :maintenance_records before running the appropriate migrations to include the foreign keys.
But something was missing... I wasn't sure what it was until I ran through my CRUD functionality with the maintenance records....
With one of my personal hobbies being purchasing, tuning, modifying, riding, and ultimately selling bikes, I wanted to be able to view all maintenance records for all of my bikes. This was important to me, because I wanted to figure out how much I might spend over the course of a year on bikes. It is really easy for me to keep track of what I have sold and the amount I net from the sale- but because there may be many transactions related to maintenance for a particular bike, it is difficult for me to track expenses.
My first solution to this problem was to try to build a new route between the user and the maintenance records, but this turned out to be a very clunky way to do it, and I couldn't figure out a means to make it simple and dynamic. There had to be a better way to do that...
Enter, the has_many :through association! As the official ruby on rails guides says so concisely: "This [has_many :through] association indicates that the declaring model can be matched with zero or more instances of another model by proceeding through a third model."
Typing fervently, I revised the association macros and altered the db schema to model the has_many :through syntax, ensuring that my user model had a reference in the bikes table, and that my bikes model had a reference in my maintenance_records table.
Now, I had to test it out by seeding a few bikes and maintenance records to make sure it was working. I wanted to see if I could test out the associations, so I placed a binding.pry in by bikes/show route.
In this route, I wanted to make sure I could look up all of a user's maintenance records, as well as find all of the maintenance records that belonged to a the particular instance of a bike.
Once I was inside my pry, I wanted to test my associations. First, I would assign an instance of my Bike class to a variable, and then use my .maintenance_records method on that instance, hoping that it would return an array with the correct maintenance record for that bike.
bike = Bike.find_by(name: 'Big Iron') bike.maintenance_records => [#<Bike:0x00007fa5323a2828 id: 8, name: "Big Iron", description: "Waltworks Hardtail 29er, 120mm", serial_number: "69854", user_id: 19>]
Okay, great! Look at what was returned! My bike object with all of its attributes, including the correct user id.
Next, using a similar process, I wanted to ensure that I could run the same .maintenance_records method on a User object, since a maintenance record belongs to both a bike and a user.
Below, you can see that I searched my User class by name to find my user object and assigned that object to a variable. Then, I used my .maintenance_records method.
user = User.find_by(name: 'Elias') user.maintenance_records => [#<MaintenanceRecord:0x00007fa53231d6f0 id: 5, name: "Suspension Tune", date: "2021-04-14", cost: 13, notes: "changed oil in fork lowers", bike_id: 8>]
As you can see above, the method returned an array containing a maintenance record object. Looking through the attributes of the object you can see that this record belongs to my bike, Big Iron that has an id of 8. That is great, because that is correct!
Now, wait a minute... doesn't a maintenance record belong to a bike AND a user?! Why isn't there an attribute for user_id in the maintenance record object if that is true?
Let's take a step back to my first association test where we were able to return the bike object...
#<Bike:0x00007fa5323a2828 id: 8, name: "Big Iron", description: "Waltworks Hardtail 29er, 120mm", serial_number: "69854", user_id: 19>
Looking through the attributes, we can see that the bike contains a column for user_id.
This, dear reader, is the the beauty of the has_many :through relationship. The User has many bikes, and bike belongs to a user. The Bike has many maintenance records, and maintenance records belong to a bike. The User has many maintenance records through bikes. This complex relationship using bikes as a join table is what allows me to associate the user with the maintenance records via bikes! A critical difference compared to my first attempt where a User had many bikes, and had many maintenance records.
This critical difference was the difference between being able to create the app I had envisioned or having a basically useless app to meet the minimum requirements for my project. While the process of trial and error was indeed challenging and time-consuming, I am glad that I was unwilling to compromise on functionality. Despite increasing my work load, I really hammered the association lessons I was struggling with and came out of the project with a working app, and more importantly a stronger foundational knowledge that will certainly help me in the future!