Sinatra is a domain specific programming language designed to allow the HTTP request handling capacity of a web application to be built quickly and easily. Sinatra’s routes elegantly and eloquently combine an HTTP method, e.g., ‘get’, ‘post’, ‘patch’, and ‘delete’, with a URL matching pattern so that when the relevant HTTP request is made to the relevant URL, a block of code is executed.
Active Record is an object-relational mapper, linking the objects of a web-application to the data contained in the related database’s tables.
When I first began working with Sinatra and Active Record, I ran into a problem: I could easily get the data contained in my database’s relational tables but I didn’t know how to get other data related to the app, e.g., the return values of a model’s methods.
I’ve since learned two ways to solve this problem and I thought it might be helpful to run through them together in one post.
Getting Table Data
Let’s say you’re creating an app to keep track of basketball games. It would make sense for the app’s database to include a table of matches and a table of players. Each match has many players and each player has many matches, so these tables stand in a many-to-many relationship. This means we’ll have a third table, a join table, to mediate this relationship. Let’s call it the player_match table.
Here’s what your tables might look like:
| PLAYERS |
| id |
| name |
| height |
| MATCHES |
| id |
| date |
| court |
| PLAYER_MATCHES |
| id |
| player_id |
| match_id |
| team |
| points_scored |
To get the data from these tables, you’ll need an application controller to set up the http request routing. This is what your application controller file might look like:
class ApplicationController < Sinatra::Base
set :default_content_type, 'application/json'
get '/players' do
players = Player.order(:name)
players.to_json
end
get '/matches' do
matches = Match.order(:date)
match.to_json
end
get '/player_matches' do
player_matches = PlayerMatch.order(:player_id)
player_matches.to_json
end
end
Each of these routes will deliver a JSON formatted object when an HTTP request is made to the correct URL. The object will contain all data on the tables of the instances of the Player, Match, and PlayerMatch classes respectively.
If there are circumstances where you don’t need to return all the data on a table, you can write routes that are more selective:
get '/players/:id' do
player = Player.find(params[:id])
player.to_json
end
This route will return only the data on the player with the id specified in the URL.
But what if you want information that is derivable from the data on the tables, but which isn’t included in the tables? What if, for example, you wanted to know which team won a particular match?
Reasons for server-side processing
You could store all the data from the tables on the client-side and carry out your calculations there. But there are some problems with this approach.
First, it’s inefficient. If you want to know which team won a match, you’d need to get all the data from the player_match tables to work with. If there have been hundreds of matches, each with at least ten players, you’d be getting data on thousands of PlayerMatch instances, almost all of which is unnecessary for the current task.
Second, it is insecure. The user will have access to any data returned from an HTTP request. If there is sensitive data on a table, then sending all the data from the table is not an acceptable option.
Third, if large chunks of data are being sent from the server to the client, the chances of this transfer being interrupted, e.g, by driving through a tunnel, increase. This would prevent from the operation being completed at all.
So, it would be good to do the processing on the server side if we can.
Attaching methods to table data
Let’s say we’ve written an instance method for the Match class, .winner, to return the name of the team that won the match. The class might look like this:
class Match < ActiveRecord::Base
has_many :player_matches
has_many :players, through: :player_matches
def winner
teamA = self.player_matches.select {|player_match| player_match.team == player_match[0].team}
teamB = self.player_matches.select {|player_match| player_match.team != player_match[0].team}
teamAPoints = teamA.sum {|player_match| player_match[:points]}
teamBPoints = teamB.sum {|player_match| player_match[:points]}
if teamAPoints > teamBPoints
teamA[0].team
else
teamsB[0].team
end
end
end
One way to get the value .winner is by attaching the method to our table data.
get '/matches' do
matches = Match.order(:date)
match.to_json({:methods => :winner})
end
This will include the result of the .winner method for each match with its corresponding match in the .json data being sent to the client.
You can also include data from a table that is connected the table in question. Let’s say we decided we wanted to bring data from all the instances of PlayerMatch along after all.
get '/matches' do
matches = Match.order(:date)
match.to_json({:include => :player_matches})
end
In this case, all of the instances of PlayerMatch related to a given match will be packaged together with the data for that match in the .json data.
This approach works, but it is still pretty inefficient. You’re getting data on all the winners when you first request the match data. You might not ever need to know who any of the winning teams are, let alone who all the winning teams are.
Method-specific routing
It would be better to set up http routing that allows the client to get the winning-team data on just the match they’re interested in, when they want it. To do this, we can set up a custom route in the application controller.
get '/matches/:id/winner' do
match = Match.find(params[:id])
winner = match.winner
winner.to_json
end
If the client sends a request to this route, specifying the id of the match, it will return just the name of the team that won the match associated with that id.
It’s worth saying a bit more about the URL in this route. Notice “:id”; this is a named parameter. You can include any named parameter in the URL to further specify the data you want returned.
For example, perhaps you have a method that takes a player’s name as an argument. You then might construct a route as follows:
get '/players/:name' do
value = Player.method(params[:name])
value.to_json
end