Ruby on Rails App: Domain With Many-to-many Relationships
Tags: ruby, rails, oop, webdev
Published: 2020-03-14 18:56:47

####Creating a domain with Many-to-many relationships: — using Active Record Associations, Rails Nested Resources and Helper Methods 

In my last Sinatra portfolio project, I created a climbing web application, **[Bolderer](https://github.com/jacqueline-lam/bolderer_sinatra_app)**, which allows `User` to log bouldering `Problems` and track their climbing progress. 

**My article on building a simple domain with Sinatra:** [Sinatra Web App: MVC, Sessions, and Routes](https://dev.to/jacquelinelam/sinatra-web-app-mvc-sessions-and-routes-52on)

### Simple Association Diagram
![Sinatra App](https://dev-to-uploads.s3.amazonaws.com/i/v52io2rt7is5yqrntfb4.png)

The goal for my Ruby on Rails project is to adopt the [Domain-driven Design (DDD)](https://airbrake.io/blog/software-design/domain-driven-design) and continue to modify my domain: stretching beyond just `User`, `Problem`, and `Style` models. I would also like to improve my domain logic with more complex database queries and data relationships.
 
I want a `user` to not only be able to track their individual climbing progress (by logging problems that they have climbed), but also share and compare their 'sends' with others. 

With this in mind, I have created a new **Bolderer Association Diagram**:
![Rails App](https://dev-to-uploads.s3.amazonaws.com/i/rqzxvxrqeid4le1a0u4u.png)
*Note: comments and rewards will be implemented in the near future* 

To allow `users` to share the same `problems` that they have climbed, I have created a Many-to-many connection between the two models.

---

## How to declare a Many-to-many association between Active Record models
A many-to-many connection is set up between the `User` model and `Problem` models, by implementing a **[`has_many :through` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association)**. The join table `Send` belongs to both the `User` and `Problem`:   

```
class User < ApplicationRecord
  has_many :sends
  has_many :problems, through: :sends
end

class Send < ApplicationRecord
  belongs_to :user
  belongs_to :problem
end

class Problem < ApplicationRecord
  belongs_to :wall
  has_many :sends
  has_many :users, through: :sends
  has_many :problem_styles
  has_many :styles, through: :problem_styles
end

class Wall < ApplicationRecord
  has_many :problems
end

class Style < ApplicationRecord
  has_many :problem_styles
  has_many :problems, through: :problem_styles
end

class ProblemStyle < ApplicationRecord
  belongs_to :problem
  belongs_to :style
end
```

With the help of Active Record `has_many :through` association, I can now use methods provided by Rails, such as:
* `@user.sends`
* `@user.problems` 
* `@send.user`
* `@send.problem`
* `@problem.users`
* `@problem.sends`

Following the set up of my model associations, I encounter the next problem: with so many models present, how can I maintain a separation of concerns and organize my routes in a relatively DRY manner? 

---

## How to Use [Nested Resources](https://guides.rubyonrails.org/routing.html#nested-resource) with appropriate RESTful URLs

#### ⚠️ Routes Previously Used in my Sinatra Application

| HTTP Verb | Route              | CRUD Action | Used for/ result                                                              |
| --------- | ------------------ | ------ | ----------------------------------------------------------------------------- |
| GET       | /                  | index  | index page to welcome user - login/ signup                                    |
| GET       | /problems          | index  | displays all problem (all problems are rendered)                              |
| POST      | /problems          | create | creates a problem; save to db                                                 |
| GET       | /problems/:id      | show   | displays one problem based on ID in the url<br>(just one problem is rendered) |
| GET       | /problems/:id/edit | edit   | displays edit form based on ID in the URL                                     |
| PATCH     | /problems/:id      | update | modifies an existing problem based on ID in the url                           |
| DELETE    | /problems/:id      | delete | deletes one article based on ID in the URL                                   |
| GET       | /users/:username | show   | display one user’s problems based on :username in the url |
*Note: excluding login/ create account routes*

As you can see, all of the `problems` belonging to a `user` were just displayed under their show page in my Sinatra Bolderer application. A `user` may have sent a `problem` without knowing that their friend has also sent it. Meanwhile, the `problem` index view was displaying multiple duplicated `problems` logged by different `users.`

In our newly drawn association diagram, a `send` can logically be considered a *child* object to a `user`, so it can also be considered a **nested resource** of a `user` for routing purposes. I am now able to document this parent/child relationship in my routes and URLs. 

#### 💡 Nested Resource Routes Used in my Rails Application

```
resources :users, only: [:index, :new, :create, :show] do 
  resources :sends
  get 'sends/sort/easiest', to: 'sends#easiest'
  get '/sends/sort/hardest', to: 'sends#hardest'
end 
```
![Routes](https://dev-to-uploads.s3.amazonaws.com/i/3uvmdr7ezvujsg3i4k8s.png)

In addition to the routes for `Sends`, this declaration will also route `Sends` to a `SendsController`, where I will be defining all the actions used to:
* Display all sends (sends#index)
* Display a form for creating a new send belonging to a user (sends#new)
* Create a new send belonging to a user (sends#create)
* Display a specific send by a user (sends#show)
* Display a form for editing a send belonging to a user (sends#edit)
* Update a specific send by a user (sends#update)
* Delete a specific send belonging to a specific user (sends#delete)

---
## Nested Route URL Helpers

Rails has also magically generated a set of nested route URL Helpers for my nested resource routes. 

Some examples of the routing helpers created include `user_sends_path` and `edit_user_sends_path`. These helpers take an instance of `User` as the first parameter (user_sends_url(@user)).

### Combining Nested Route URL helpers with `link_to` helper

I can now easily use Rails `link_to` helper method and the named helpers for our nested routes to create a link to a specific `User`'s `Send` show page:

*🔗 Creating a link in plain HTML:* 

`<a href="/users/#{@send.user.id}/sends/#{@send.id}">See this send</a>`

*🔗 V.S. Using Rails helper method `link_to` + named helpers for nested routes:*

`<%= link_to 'See this send',  user_send_path(@send.user, @send) %>`

**Note: Rails is able to extract the `id`s of the `@send.user` and `@send` passed into the `user_send_path`, and it will redirect us to the show page of this specific `send`.**

---
## 🔎 Class-level Active Record [Scope methods](https://guides.rubyonrails.org/active_record_querying.html#scopes)

Lastly, I find it immensely helpful to use Rails scope methods for improving my domain logic, as it allows me to perform complex database queries:

**Problem model**
```
  scope :sort_by_date, -> { order('created_at desc') }
  scope :sort_by_grade, -> { order('grade desc') } 
```
**Send model**
```
  scope :sort_by_date, -> { order('date_sent desc') }
  scope :sort_by_grade_desc, -> (user) {
    where(user_id: user).joins(:problem).order('grade desc')
  } 
  scope :sort_by_grade_asc, -> (user) {
    where(user_id: user).joins(:problem).order('grade asc')
  } 
```
**User model**
```
  # query user table for user who climbed the hardest graded problem 
  scope :best_climber, -> { joins(:problems).order('grade desc').distinct.limit(1).first } 
```
A `user` can now easily browse `problems` and `sends`: 
* **`User`'s `sends`**: sort by most recent sends, easiest to hardest sends, and hardest to easiest sends
* **`Problems`**: sort by most recently created problems, easiest to hardest problems, and hardest to easiest problems
* **`User`**: Display the crusher who climbed the hardest problem  

---

## Takeaways

* With the help of Rails, I can easily create Active Record associations between different models while developing my domain.
* Implement nested resources, a powerful tool, to represent the parent/child relationships in my domain (A `Send` belongs to a `User`) and to keep my routes tidy.
* Use the 🔗nested route URL helpers to easily link to different views
* Use 🔎scope methods to perform complex database queries.