DEV Community

william-luck
william-luck

Posted on

Many-to-Many Associations in Active Record Example

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:

Image description

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
Enter fullscreen mode Exit fullscreen mode
class Traveler < ActiveRecord::Base 

    has_many :visits
    has_many :countries, through: :visits
Enter fullscreen mode Exit fullscreen mode
class Visit < ActiveRecord::Base
    belongs_to :country
    belongs_to :traveler
Enter fullscreen mode Exit fullscreen mode

Entity Relationship Diagram

When I was thinking of ideas for the relationships needed to build this application, I originally came up with this ERD:

Image description

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
Enter fullscreen mode Exit fullscreen mode

'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:

Image description

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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)