Ruby on Rails - quick introduction
Introduction to Ruby on Rails presenting CRUD, database relations, mailer, and web sockets communication.
Daniel Gustaw
• 12 min read
In 2019, I rewrote a certain medical system from Rails to PHP, and in 2021 from Rails to NodeJS. Perhaps you are also encountering Rails-based systems that are losing maintenance. This introduction will help you quickly familiarize yourself with the basics of this framework.
We will write a blog completely from scratch. I would like to point out that I am not very familiar with either Ruby or Rails, so instead of an extensive introduction, we have a recreation of my learning process.
Assumptions:
- we are using linux (arch)
Setting up the application - CRUD
We will start with installing the appropriate version of ruby.
curl -sSL https://get.rvm.io | bash -s stable --rails
rvm
is a tool analogous to nvm
- it allows you to manage the interpreter version, which is exceptionally useful when working with systems that use different versions of interpreters. You can read about it here:
RVM: Ruby Version Manager - Installing RVM
We create the application with the following command:
rails new weblog && cd weblog
This command takes a long time because it requires the installation of all gem
packages and compilation of node-sass
.
The next step is to automatically generate code to perform CRUD operations on a post. Posts will have a title and content.
rails generate scaffold post title:string body:text
This command generates a large number of files:
One of them is the database migration, which is written in db/migrate/20210418121400_create_posts.rb
and looks like this:
class CreatePosts < ActiveRecord::Migration[6.1]
def change
create_table :posts do |t|
t.string :title
t.text :body
t.timestamps
end
end
end
To synchronize the database with the result of this migration, we enter
rails db:migrate
Here you may ask the question: “Which database?”. In the file config/database.yml
we can see the configuration that indicates that by default it is sqlite
. In the file db/schema.rb
there is the database schema.
This is a good place for a digression. While migrating systems based on Ruby on Rails, I wondered why the production environment uses “sqlite”; I thought someone deliberately configured it this way. It turns out that it was enough not to change the configuration in this file. Another issue that occupied my mind two years ago was the “updated_at” field in tables that didn’t handle editing. Seeing “updated_at” and lacking documentation, I thought there was a process for editing these tables; however, this is also a consequence of the default “rails” configuration, which everywhere adds these columns.
To start the server, we use the command
rails server
A huge advantage of Rails is that we can already use a working CRUD at the link
http://127.0.0.1:3000/posts
After manually creating a post, we get:
What is even more pleasant is that we also have an “api” available at /posts.json
Unfortunately, the attempt to create a post via the API.
http POST localhost:3000/posts.json title="Hej" body="Ok"
ends with an error
Can't verify CSRF token authenticity.
To disable “CSRF” protection in the app/controllers/application_controller.rb
file, configure the protect_from_forgery
option.
class ApplicationController < ActionController::Base
protect_from_forgery with: :null_session
end
Now the post saving through the API works. Both
http POST localhost:3000/posts.json title=ok
how and
http POST localhost:3000/posts.json body=ok
will post their entries without validating their correctness.
To enforce the presence of the title
parameter in the post, in the file app/models/post.rb
we add the validates_presence_of
flag.
class Post < ApplicationRecord
validates_presence_of :title
end
Thanks to it, it will be impossible to add posts without a title both on the page
how and through API
Debugging - Rails Console
A very useful tool when working with Ruby on Rails is the console available by entering the command:
rails console
It allows for interactive access to data using the Ruby language and objects defined in Rails. For example, we will see the first post by entering
Post.first
To get all posts we write
Post.all
Posts created from yesterday to tomorrow will be received by writing
Post.where(created_at: Date.yesterday..Date.tomorrow)
It can be easily transformed into an SQL query by adding the to_sql
property at the end.
Post.where(created_at: Date.yesterday..Date.tomorrow).to_sql
To create a new post we write
Post.create! title: 'Hello', body: 'World'
Relationships Between Tables
A typical example of a relationship regarding posts is comments. We do not need the same controllers and views for them as for posts, so instead of scaffold
, we will use the resource
flag for generation.
rails generate resource comment post:references body:text
We can see the full list of available generators by entering the command:
rails generate
or by reading the documentation
The Rails Command Line — Ruby on Rails Guides
Meanwhile, we will return to the files generated by the resource
option.
A migration has been created here again, this time containing:
class CreateComments < ActiveRecord::Migration[6.1]
def change
create_table :comments do |t|
t.references :post, null: false, foreign_key: true
t.text :body
t.timestamps
end
end
end
To execute it, we enter
rails db:migrate
Let’s now talk about routing. There is no point in ever asking for all comments. They are always related to the post they pertain to. So in the config/routes.yml
file, we replace the adjacent occurrences.
Rails.application.routes.draw do
resources :posts
resources :comments
end
to configuration that allows comments to be nested in the post
Rails.application.routes.draw do
resources :posts do
resources :comments
end
end
Displaying the routing is possible thanks to the command:
rails routes
As for the direction of the relationship, at this moment comments belong to posts as described in the file app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :post
end
But posts do not have a designated relationship with comments, which we will fix by adding has_many
to app/models/post.rb
class Post < ApplicationRecord
has_many :comments
validates_presence_of :title
end
In the console we can now create a sample comment
Post.second.comments.create! body: "My first comment to second post"
To display comments and add them, we will write helper view fragments (partials). app/views/comments/_comment.html.erb
will be used to display a single comment.
<p><%= comment.body %> -- <%= comment.created_at.to_s(:long) %></p>
On the other hand, app/views/comments/_new.html.erb
will be the form for creating a comment.
<%= form_for([ @post, Comment.new], remote: true) do |form| %>
Your comment: <br/>
<%= form.text_area :body, size: '50x2' %><br/>
<%= form.submit %>
<% end %>
We will attach them in the single post view by adding the code to the file app/views/posts/show.html.erb
<hr>
<h2>Comments (<span id="count"><%= @post.comments.count %></span>)</h2>
<div id="comments">
<%= render @post.comments %>
</div>
<%= render 'comments/new', post: @post %>
Now our post view will look as follows
Although it looks ready to go, the comment addition feature is still unavailable. We only prepared the view, but the logic to handle saving comments to the database and linking them to posts is missing.
To integrate it, we need to handle comment creation in the controller app/controllers/comments_controller.rb
class CommentsController < ApplicationController
before_action :set_post
def create
@post.comments.create! comments_params
redirect_to @post
end
private
def set_post
@post = Post.find(params[:post_id])
end
def comments_params
params.required(:comment).permit(:body)
end
end
Let’s take a close look at it. It starts with the before_action
option, which sets the post based on the parameter from the url
. Then in create
, we use this post to create a comment, its parameters come from comments_params
, which retrieves them from the request body.
Next, there is a redirection to the posts page. It works very well on the page.
But if we want to create posts from the API level, every time we are redirected to the post, we will see it without comments. If we replace
redirect_to @post
in the controller using instructions analogous to that for the post
respond_to do |format|
if @post.save
format.html { redirect_to @post, notice: "Comment was successfully created." }
format.json { render :show, status: :created, location: @post }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
we will get an error
It is so because now comments require structuring when arranging them in a JSON file. This is resolved thanks to the fantastic library jbuilder
.
By creating the file app/views/comments/show.json.jbuilder
with the content
json.partial! "posts/post", post: @post
json.comments @post.comments, :id, :body, :created_at
we will configure the server to respond with the post view containing a list of all comments corresponding to it after a comment is created. This is a view that corresponds to what we see in the HTML version, although it does not conform to REST principles.
If we wanted to display this specific comment, we can use the syntax
def create
comment = @post.comments.create! comments_params
respond_to do |format|
if @post.save
format.html { redirect_to @post, notice: "Comment was successfully created." }
format.json { render json: comment.to_json(include: [:post]) }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
in the controller. Then in the response view we will see the comment along with the post.
More about formatting can be read here:
Sending emails
A very common function in web services is sending emails in response to certain events. Instead of rewriting the code, we will use a generator:
rails generate mailer comments submitted
This is an emailer sending a greeting. The first thing we will do is configure the data that it will inject into the templates. In the comments_mailer.rb
file, we write the code:
class CommentsMailer < ApplicationMailer
def submitted(comment)
@comment = comment
mail to: "[email protected]", subject: 'New comment'
end
end
In app/views/comments_mailer
we have two template files. For the HTML view, it is the submitted.html.erb
file. We will modify it so that using the previously defined partial, it shows the new comment:
<h1>New comment on post: <%= @comment.post.title %></h1>
<%= render @comment %>
In the submitted.text.erb
file, we can no longer use render
, so we will simplify the text view to the form:
New comment on post: <%= @comment.post.title %>: <%= @comment.body %>
What’s amazing about Rails is that we have a ready-made view to preview these emails without having to send them. To use it, we just need to specify the comment we will display. For this purpose, in the file test/mailers/previews/comments_mailer_preview.rb
the line
CommentsMailer.submitted
we change to
CommentsMailer.submitted Comment.first
At the address
http://localhost:3000/rails/mailers/comments\_mailer/submitted
We can see a preview of this email
However, we cannot expect this email to be sent immediately. To include its sending, we need to add a line.
CommentsMailer.submitted(comment).deliver_later
in the comments controller. The entire controller should now look like this:
class CommentsController < ApplicationController
before_action :set_post
def create
comment = @post.comments.create! comments_params
CommentsMailer.submitted(comment).deliver_later
respond_to do |format|
if @post.save
format.html { redirect_to @post, notice: "Comment was successfully created." }
format.json { render json: comment.to_json(include: [:post]) }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
private
def set_post
@post = Post.find(params[:post_id])
end
def comments_params
params.required(:comment).permit(:body)
end
end
The “deliver_later” flag allows you to attach an email sending to the internal Ruby on Rails loop, which will send it as soon as possible without blocking the execution of the rest of the code. Creating a comment still won’t send the email to the actual mail, but in the console, we will see that such an action would have been taken if the sending were fully configured.
We will not go that way, but if you want to complete the configuration, read about smtp_settings
and delivery_method
in the documentation:
Action Mailer Basics — Ruby on Rails Guides
Now we will move on to real-time communication.
Cable - communication via web socket
To use real-time communication, we need a channel. We will generate it with the command:
rails generate channel comments
In the file app/channels/comments_channel.rb
containing:
class CommentsChannel < ApplicationCable::Channel
def subscribed
# stream_from "some_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
adding the broadcast
method
def self.broadcast(comment)
broadcast_to comment.post, comment:
CommentsController.render(partial: 'comments/comment', locals: { comment: comment })
end
we will also make a simplification that the subscription will only apply to the latest post. Our goal is to show the basics of Rails, so we will focus on bringing the channel mechanism to presentation, skipping this aspect. As part of this simplification, we write
def subscribed
stream_for Post.last
end
To enable message sending to the browser, we add the line
CommentsChannel.broadcast(comment)
with the emailer included in the comments controller.
A file with the channel configuration app/javascript/channels/comments_channel.js
will be attached to the browser. We set it up so that in response to a comment being attached to the publication (channel), it should be added to the end of the thread, and the comment counter should increase by 1:
received(data) {
const commentsElement = document.querySelector('#comments');
const countElement = document.querySelector('#count');
if (commentsElement) {
commentsElement.innerHTML += data.comment
}
if (countElement) {
countElement.innerHTML = String(1 + parseInt(countElement.innerHTML))
}
}
The effect is as follows:
For further study, I recommend the following materials:
Other articles
You can find interesting also.
Infrastructure as Code (Terraform + Digital Ocean)
In this post, I show how to set up servers using the terraform command line.
Daniel Gustaw
• 3 min read
Publishing an update of the package in the AUR repository
Learn how to publish package updates in the Arch Linux user repository.
Daniel Gustaw
• 3 min read
CodinGame: Derivative Time - Part 1, Recursion (Typescript)
Solution of CodinGame exercise. Simple recursion example with typescript. Formula representation inspired by lisp.
Daniel Gustaw
• 16 min read