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
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:
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
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
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
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
'ssends
: 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 aUser
) 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.
Top comments (3)
Nice work on breaking down these Rails concepts! One question...what software do you use to create your association diagrams? Thanks!
Thanks, Smith! I created my association diagrams on app.diagrams.net.
Thanks so much!