Ruby on Rails App: Domain With Many-to-many Relationships

jacquelinelam

Jacqueline Lam

Posted on March 14, 2020

Ruby on Rails App: Domain With Many-to-many Relationships

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, 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

Simple Association Diagram

Sinatra App

The goal for my Ruby on Rails project is to adopt the Domain-driven Design (DDD) 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
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. 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
Enter fullscreen mode Exit fullscreen mode

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

Routes

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 ids 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

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

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

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

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.
💖 💪 🙅 🚩
jacquelinelam
Jacqueline Lam

Posted on March 14, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related