Sinatra and ActiveRecord Associations
Frameworks, such as Sinatra and ActiveRecords, are great. They abstract various layers of common coding patterns allowing you to become a more efficient programer. Without an understanding of the ‘magic’ behind these layers of abstraction, however, your code will lead to unexpected errors.
While working on a Sinatra lab last week my partner and I encountered an unexpected error that helped to solidify my understanding of the power of ActiveRecord.
For this lab we were required to build a history site, that included three models: Landmark, Title and Figure. The relationship between these models were as follows:
- A landmark belongs_to a figure
- A figure has_many landmarks
- Title and figures have a “many_to_many” relationship (this relationship required a join table, that we called title_figures).
Once we had our tables and models set up, we started on our views and controllers. First we tackled the following two methods in the figures_controller and corresponding view files
- get ‘/figures/new’
- post ‘/figures’
The new form required the following 5 inputs: a name field, the option to select an existing landmarks from the database, the option to create and associate a new landmark, the option to select an existing title from the database and the option to create and associate a new title (remember, a figure has_many landmarks and has_many titles). The get ‘/figures/new’ method rendered the following view:
Once a user inputs the information in the above form, the post ‘/figures’ method processes the informations and saves it to the database. Below is a screenshot of our initial post ‘/figures’ method (this was our first draft and the code still needed to be refactored):
In the above method we created a instance of the Figure class and then proceeded to check the fields that were created (the user was not required to submit something for the landmarks and titles fields). The logic seemed to make sense so we went through the motions of creating a new figures.
Did a new figure get created? Yes.
Were the attributes and associations properly saved? No.
From the code above we can see that there are three titles associated with the figure that we created. “The Mayor” title, which was already saved in the title database, was associated twice with the figure. “The Archmaester” title, the landmarks and the name were all properly saved. How did this happen?
Looking at the ActiveRecord TRAIL we can see that we created a row in the figure_titles join table on two separate occasions.
In the post ‘/figures’ method in our figures_controller, we wrote out a conditional statement that would create a new instance of the title_figures table if someone selected an existing title. The method was also doing this at an earlier point in the code.
The first line of code in our method creates a new instance of the figure and passes in an argument params[:figure].
And what exactly are those params…
So.. when we create the instance of the figure we are passing in the selected landmark_id(s) and title_id(s) (note: the params[:figure] does not include what was entered for a new landmark/title. Thanks to ActiveRecord, this means that we were not only creating an instance of a figure, but also associating that title and landmark. So how come we were only doubling up on the selected title and not landmark?
Let’s go back to our model relationships. Remember, figures and titles have a many_to_many relationship.
Since we defined these relationships in our class model and inherited from ActiveRecord, the model relationships are automatically created when a new instance of a figure is created with the correct attributes.
A figure knows its titles through the figure_titles join table. With a join table, we can create multiple instances in that table with the same title_id and figure_id because the method we wrote is not checking for figure_titles.id.
The figure to landmark relationship, however, is a has_many and belongs_to relationship. In this type of relationship there is no join table, and our method was able to check for the landmark_id. Thus, no duplicates are created as the method is able to find the unique identifier (and essentially override the original instance).
In order to fix this issue we simply removed the two conditionals that checked if a user had selected an existing landmark or title when creating their figure.