Many-to-Many Associations in Active Record Example
Tags: webdev, ruby, react, javascript
Published: 2022-10-18 17:49:25

#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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cx61fm5yc64autsfhazz.gif)

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

```ruby
class Country < ActiveRecord::Base

    has_many :visits
    has_many :travelers, through: :visits
```
```ruby
class Traveler < ActiveRecord::Base 
    
    has_many :visits
    has_many :countries, through: :visits
```
```ruby
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:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xk3jpotr794zejsa539b.png)

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: 
```ruby
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: 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bs732cfhf1b81gnog6oz.png)


#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:
```ruby
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: 
```ruby
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: 
```javascript
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.