Background
Using Ruby, Active Record, Sinatra, and a React frontend, I created a traveler tracker for travelers in a country at a given time. Users can scroll through a list of all countries, see the number of travelers in that country, select the country for a list of current travelers, and see the individual visits for the traveler in that country. Additionally, users can see the traveler profile, and there are options to add a new traveler or a new visit for an existing traveler:
The application makes use of three backend models, Country, Visit, and Traveler. A given country has many travelers, through the visits recorded in that country. Similarly, a traveler has many countries, through visits recorded in each of the countries they visited. Each visit belongs to both a country and a traveler (many-to-many association), and only one can traveler can stay at an an accommodation at a time (which is recorded as a visit):
class Country < ActiveRecord::Base
has_many :visits
has_many :travelers, through: :visits
class Traveler < ActiveRecord::Base
has_many :visits
has_many :countries, through: :visits
class Visit < ActiveRecord::Base
belongs_to :country
belongs_to :traveler
Entity Relationship Diagram
When I was thinking of ideas for the relationships needed to build this application, I originally came up with this ERD:
The Visits are established correctly as the join table, but the Travelers has an unnecessary column as current_country_id, with no association to a country established other than the ID. However, each traveler will have a list of countries they have visited, and you could find the current country of a traveler by just calling #countries[-1]
on an instance of a traveler, as such:
class Traveler < ActiveRecord::Base
has_many :visits
has_many :countries, through: :visits
def current_country
self.countries[-1]
end
'from_country_id' is a separate column, and this is used for nationality. I kept this an integer to correspond with the country IDs of the countries table, instead of having to match a nationality string exactly with the country name. We could just find a traveler's nationality by calling #find(#traveler.from_country_id)
on an instance of a country. This leaves us with the following ERD:
Finding travelers currently in a country
While we could easily query which country a given traveler is currently in, it is a bit more difficult to find all the travelers that are currently in a given country. Calling #travelers
on an instance of a country will return all the travelers that were in that country ever, through the series of visits belonging to each traveler in that country. Similarly, calling #visits
on an instance of a country will return all the visits in that country ever, not necessarily the visits of the travelers currently in that country. We can use the #current_country
method defined above to help with this.
As calling #travelers
on an instance of a country returns all travelers in that country ever, we can iterate through that array of travelers to see if their last country_id corresponds with the country_id of the country instance. If this is the case, we can push those travelers to a new array, and return that array to get all the travelers currently in that country:
class Country < ActiveRecord::Base
has_many :visits
has_many :travelers, through: :visits
def travelers_currently_in_country
travelers_in_country = []
all_travelers = self.travelers
all_travelers.each do |traveler|
current_country = traveler.current_country
if current_country.id == self.id
travelers_in_country << traveler
end
end
travelers_in_country.uniq
end
How is this used in the application controller?
When I click on a country in the application, we want the travelers in the country to display immediately below the list of countries. I have defined this route in the application controller for get requests to receive JSON of the travelers in a given country, using the above #travelers_currently_in_country
method:
get '/travelers_in_country/:id' do
selected_country = Country.find(params[:id])
selected_country.travelers_currently_in_country.to_json
end
We make use of the params hash here, so that the route is dynamic and accepts a country ID. The params hash is created when a request is made to that endpoint. As we are not sending any data in a post or patch request, the dynamic id is the only thing that we need to use with the params hash. We use Country.find(params[:id])
to find the matching country in the database, and then call the #travelers_currently_in_country
method on that country.
In the frontend, the fetch request looks like this:
fetch(`http://localhost:9292/travelers_in_country/${selectedCountry}`)
.then((r) => r.json())
.then(travelersInCountry => setTravelersInCountry(travelersInCountry))
We pass the ID of the selected country in the url, the application receives the response from the API, and we set state to have the list of traveler's re-render in the frontend. We get an array of objects in this response, and we can then call .map
to return JSX to display the travelers in that country.
Top comments (0)