Ruby on Rails is an open source full-stack web framework built in Ruby. It's optimized for programmer happiness with Convention over Configuration (CoC) and Don't Repeat Yourself (DRY) principles.
As always, the Documentation is our best friend.
We are going to learn Rails by building an example app. Let's say we want to create a simple blog with these specs:
- It will have some static pages (home, about, contact)
- It will have a page with all the posts.
- Users will be able to see all the posts, create a new post, edit it, and delete it.
- Users will also be able to see comments on posts and write new comments.
$ gem install rails -v 4.2.6
Let's quit and restart the terminal when it's done. On Ubuntu, we need to run this as well:
$ sudo apt-get install nodejs
To create a new rails application in the current folder, let's run this terminal command:
$ rails new blog
This will create a new folder named blog
After creating the app, let's go to this folder and launch Sublime Text:
$ cd blog
$ stt
Rails follows the Model-View-Controller (MVC) pattern
The application code is in /app
, for now we will focus on app/controllers
, app/models
and app/views
The other file we will care about for now is config/routes.rb
where we will describe all the application's routes.
Let's run this terminal command from our rails app folder (in a new console tab, preferably):
$ rails server # or: rails s
if we go to http://localhost:3000/ on our browser, we will see the rails home page:
The Rails console is IRB with the rails environment loaded. Let's run this terminal command from our rails app folder (in a new console tab, preferably):
$ rails console # or: rails c
💡 Tips
A few usefull commands for the rails console:
# From the rails console:
irb(main)> reload! # will reload the console with the new code we wrote
irb(main)> exit # exit the console
# On launch:
$ rails console --sandbox # all the changes will be rolled back at exit
Rails integrates some generators that allow us to quickly generate classes. Let's generate a pages controller for our static pages:
$ rails generate controller pages home about contact
# create app/controllers/pages_controller.rb
# route get 'pages/contact'
# route get 'pages/about'
# route get 'pages/home'
# invoke erb
# create app/views/pages
# create app/views/pages/home.html.erb
# create app/views/pages/about.html.erb
# create app/views/pages/contact.html.erb
# ...
# < other stuff >
We can now navigate to those pages:
- http://localhost:3000/pages/home
- http://localhost:3000/pages/contact
- http://localhost:3000/pages/about
The generator created:
-
a new controller class in
app/controllers/pages_controller.rb
:# app/controllers/pages_controller.rb class PagesController < ApplicationController def home end def about end def contact end end
-
3 routes in
config/routes.rb
:# config/routes.rb Rails.application.routes.draw do get 'pages/home' get 'pages/about' get 'pages/contact' end
-
and 3 view files in
app/views/pages
like this one:<!-- app/views/pages/home.html.erb --> <h1>Pages#home</h1> <p>Find me in app/views/pages/home.html.erb</p>
At this point, we might wonder how Rails does this all magically. That is all the power of Convention over Configuration: Rails made some choices for us, if we are happy with them, we don't need to configure anything.
We can see a pattern here:
- Controller names are plural: PagesController
- The router will also route by default:
get 'pages/home'
will execute thehome
method of thepages
controller. - the controller will render HTML views by default, and will seek them in a folder with the controller name (pages) and a filename being the method name (home.html.erb)
- As we can see, there is no
initialize
method in this pages controller, norattr_reader
,attr_writer
orattr_accessor
, norequire
norrequire_relative
. This is all defined with some defaults in the mother classe that controllers (and later models as we will see soon) inherit from.
Note: .erb
files (Embedded RuBy) will render an HTML file, we will understand this later.
We want to modify the routes this way:
- http://localhost:3000/ should show us the home page
- http://localhost:3000/contact should show us the contact page
- http://localhost:3000/about should show us the about page
We will have to modify config/routes.rb
:
# config/routes.rb
Rails.application.routes.draw do
# this will give us a home page on http://localhost:3000/
root 'pages#home'
# For other routes, the syntax is:
# verb 'path' => 'controller#method'
get 'about' => 'pages#about'
get 'contact' => 'pages#contact'
end
Let's check if it worked by running this terminal command:
$ rake routes
# Prefix Verb URI Pattern Controller#Action
# root GET / pages#home
# about GET /about(.:format) pages#about
# contact GET /contact(.:format) pages#contact
Now we can style our HTML files. First, we can see that Rails uses a Layout in app/views/layouts/application.html.erb
as a general architecture for all HTML files. We only need to write the body in our HTML files. Of course we can also modify the Layouts and even have different layouts for different purposes.
As we saw, our view files are .html.erb
file, which allows us to include Ruby code in the views:
<!-- use '<% %>' to write ruby code -->
<% time = Time.now %>
<h1>Welcome!</h1>
<!-- use '<%= %>' to print -->
<p>The time is <%= time %></p>
Instance variables declared in the controller method are accessible in the view:
class PagesController < ApplicationController
def home
@today = Date.today
end
end
<!-- app/views/pages/home.html.erb -->
<h1>Welcome!</h1>
<p>Today is <%= @today %></p>
We can write our css in app/assets/stylesheets/application.css
, and our javascript in app/assets/javascripts/application.js
.
📝 Note
By default, Rails controllers render HTML views. For your Camp Project, the Rails application will communicate with other apps (mostly the AngularJS client), not with humans. Humans like HTML, but computers prefer another format: JSON. For this blog example, we will render HTML views though, so that we can have a visual example.
All this is nice for static content. But how are we going to manage our blog posts and comments? They will be created by the app's users, and we will have to store them in the database.
That is where we need Models, the classes that will interact with the database. Model classes will be ActiveRecord classes.
Active Record is a pattern to store data in relational databases.
It will allow us to store an object as a row of a table in a relational database.
In Ruby, it's implemented in the activerecord
gem, that is included by default in Rails. To put it simple, ActiveRecord allows us to write Ruby code instead of SQL to persist data and query a relational database.
A migration is a modification of the database structure. For example, create a new table, add a column to a table, rename a column, etc.
ActiveRecord allows us to write our migrations in Ruby, not SQL. As we will see soon, Rails provides some generators for migrations.
Basic rake tasks:
$ rake db:create # create a blank database
$ rake db:drop # get rid of the database (:warning: you lose all your data!)
$ rake db:migrate # run all the pending migrations
$ rake db:rollback # rollback the last migration
$ rake db:seed # seed the database with data from db/seeds.rb
Back to our models. We will need two models, Post
and Comment
.
Let's start with Post
, it will have two attributes: title
and content
. Rails provides a usefull generator:
$ rails generate model Post title:string content:text
# invoke active_record
# create db/migrate/20160615082457_create_posts.rb
# create app/models/post.rb
# ...
# <other stuff>
The generator created:
-
a new model class in
app/models/post.rb
:# app/models/post.rb class Post < ActiveRecord::Base end
It is an ActiveRecord model as it inherits from
ActiveRecord::Base
. This will allow us to use all the Ruby methods to persist objects and query the database. -
a migration file in
db/migrate/20160615082457_create_posts.rb
:# db/migrate/20160615082457_create_posts.rb class CreatePosts < ActiveRecord::Migration def change create_table :posts do |t| t.string :title t.text :content t.timestamps null: false end end end
By convention, a model name is singular (and CamelCase as in Ruby convention), and it corresponds to it's snake_case pluralized table name in the database: Model Post
corresponds to the posts
table in the database.
Let's not forget to run the migration:
$ rake db:migrate
We can also generate custom migration, for example to modify our Post model:
$ rails generate migration AddAuthorNameToPosts
# invoke active_record
# create db/migrate/20160615085144_add_author_name_to_posts.rb
We can then write our code in the change
method:
# db/migrate/20160615085144_add_author_name_to_posts.rb
class AddAuthorNameToPosts < ActiveRecord::Migration
def change
add_column :posts, :author_name, :string
end
end
Read the Documentation to see everything you can do in migrations.
Let's look again at the specs for our blog posts:
Users will be able to see all the posts, create a new post, edit it, and delete it.
Those are the basic actions of any resources in an app: Create - Read - Update - Delete (CRUD). In detail, we will need the following actions:
index
Read all the postsshow
Read one post- Create one post (2 actions):
new
Show form with post empty fieldscreate
Persist new Data in the database
- Update one post: (2 actions):
edit
Show form with post existing data in fieldsupdate
Persist updated Data in the database
destroy
Delete one post
Let's create a PostsController (remember the plural convention) with those 7 methods:
$ rails generate controller Posts index show new create edit update destroy
Before we complete the controller methods, let's have a look at the routes. This is how we should write those routes:
# config/routes.rb
Rails.application.routes.draw do
get 'posts/' => 'posts#index'
get 'posts/:id' => 'posts#show'
get 'posts/:id/edit' => 'posts#edit'
get 'posts/new' => 'posts#new'
post 'posts/' => 'posts#create'
patch 'posts/' => 'posts#update'
delete 'posts/:id' => 'posts#destroy'
# [...]
end
But actually, Rails has a shortcut for those 7 routes:
# config/routes.rb
Rails.application.routes.draw do
resources :posts
# [...]
end
We can also use a subset of those 7 routes:
# config/routes.rb
Rails.application.routes.draw do
resources :posts, only: [:show, :index]
# [...]
end
or
# config/routes.rb
Rails.application.routes.draw do
resources :posts, except: [:edit, :update, :destroy]
end
Let's complete these methods now. The GET
methods are pretty straightforward:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
# GET /posts
def index
@posts = Post.all
end
# GET /posts/:id
def show
@post = Post.find(params[:id])
end
# GET /posts/new
def new
@post = Post.new
end
# POST /posts
def create
end
# GET /posts/:id/edit
def edit
@post = Post.find(params[:id])
end
# PATCH /posts/:id
# PUT /posts/:id
def update
end
# DELETE /posts/:id
def destroy
end
end
For create
and update
, we want to persist the data and then render the created/updated post to the user. But wait! We already have a view that displays one method that will render one post to the user, the show
method. So we can redirect to this method in the end of create
and update
.
To know the path to redirect to, we can look at the routes and add a suffixe _path
to the route name. We can also pass params if the method requires it:
$ rake routes
# Prefix Verb URI Pattern Controller#Action
# posts GET /posts(.:format) posts#index
# POST /posts(.:format) posts#create
# new_post GET /posts/new(.:format) posts#new
# edit_post GET /posts/:id/edit(.:format) posts#edit
# post GET /posts/:id(.:format) posts#show # <- we want this one
# PATCH /posts/:id(.:format) posts#update
# PUT /posts/:id(.:format) posts#update
# DELETE /posts/:id(.:format) posts#destroy
# about GET /about(.:format) pages#about
# contact GET /contact(.:format) pages#contact
For the show method, we would have post_path
and we need to pass at least the :id
parameter: post_path(id: @post.id)
we can also pass the entire object, only it's id is going to be taken into account: post_path(@post)
.
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
# [...]
# POST /posts
def create
@post = Post.new(params[:post])
@post.save
# Will raise ActiveModel::ForbiddenAttributesError
redirect_to post_path(@post)
end
# PATCH /posts/:id
# PUT /posts/:id
def update
@post = Post.find(params[:id])
@post = Post.update(params[:post])
# Will raise ActiveModel::ForbiddenAttributesError
redirect_to post_path(@post)
end
end
📝 Note As you can see, we are using some Post
instance and class methods that we did not define in the Post
class: #save
, #find
, #update
. Those are all ActiveRecord methods that translates to SQL to persist data or query the database.
We will apply the same principle for destroy
, after a resource is destroyed, we will redirect to the list of posts index
:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
# [...]
# DELETE /posts/:id
def destroy
@post = Post.find(params[:id])
@post.destroy
redirect_to posts_path
end
end
By default, Rails will raise an Exception when we try to create or update a post using the params. Why? It's a protection feature. Imagine we have a User
model with a boolean attribute called admin
. In this case, we will probably not allow a user to define himself if he is admin or not, which he could do by sending a POST or PATCH request to the correct endpoint.
So we need to whitelist what params are allowed in update/create. Here is how to do it:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
# [...]
def create
@post = Post.new(post_params)
@post.save
redirect_to post_path(@post)
end
def update
@post = Post.find(params[:id])
@post.update(post_params)
redirect_to post_path(@post)
end
private
def post_params
params.require(:post).permit(:title, :content, :author_name)
end
end
As we can see, some of the code is duplicate. Rails integrates filters, for example the before_action
filter who will be executed before the execution of the method. Let's use it to make our code DRY:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :set_post, only: [:show, :edit, :update, :destroy]
def show
end
def edit
end
def update
@post.update(post_params)
redirect_to post_path(@post)
end
def destroy
@post.destroy
end
private
def set_post
@post = Post.find(params[:id])
end
end
Validations are another feature of ActiveRecord: we can check the validity of an object before saving it to the database.
We can add some validations to our Post model:
# app/models/post.rb
class Post < ActiveRecord::Base
validates :content, presence: true
validates :title, presence: true, uniqueness: true
end
This means that a Post won't save unless it has a content and a title, and that no other currently saved post already have this title. Other validations.
We need to update our controller accordingly:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
# [...]
def create
@post = Post.new(post_params)
# if @post is valid, @post.save saves the post and returns @post,
# otherwise it does not save and returns false
if @post.save
redirect_to post_path(@post)
else
# we can use @post.errors in the view
render :new
end
end
def update
# #update reacts the same as #save
if @post.update(post_params)
redirect_to post_path(@post)
else
render :edit
end
end
end
Here is our final Post
model and PostsController
controller class:
# app/models/post.rb
class Post < ActiveRecord::Base
validates :content, presence: true
validates :title, presence: true, uniqueness: true
end
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :set_post, only: [:show, :edit, :update, :destroy]
# GET /posts
def index
@posts = Post.all
end
# GET /posts/:id
def show
end
# GET /posts/new
def new
@post = Post.new
end
# POST /posts
def create
@post = Post.new(post_params)
if @post.save
redirect_to post_path(@post)
else
render :new
end
end
# GET /posts/:id/edit
def edit
end
# PATCH /posts/:id
# PUT /posts/:id
def update
if @post.update(post_params)
redirect_to post_path(@post)
else
render :edit
end
end
# DELETE /posts/:id
def destroy
@post.destroy
redirect_to posts_path
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :content, :author_name)
end
end
Now that we have posts, let's implement the comments. The Comment
model is going to be related to the Post
model in a one-to-many relationship. If you remember the SQL course, we want a post_id
column in the comments
table:
$ rails generate model Comment post:references content:string
$ rake db:migrate
Associations are yet another feature of ActiveRecord: it allows us to describe in the Model class how objects are related to each others, and use those to access the related models.
Let's write the associations in the models:
class Comment < ActiveRecord::Base
belongs_to :post
end
class Post < ActiveRecord::Base
has_many :comments, dependent: :destroy
# [...]
end
Now we can easily access the comments of a post, and the post of a comment:
post = Post.first
post.comments
# => SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 1]]
comment = Comment.first
comment.post
# => SELECT "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT 1 [["id", 1]]
Read more about associations in the Documentation.
Before we generate a CommentsController, let's think about it's methods:
- We don't want to list all the comments. It would make no sense without the relative Post. However, we want a page with all the comments of a single post. So we still want an
index
method. - We don't want to show a single comment, it would make no sense without context.
- To keep it simple, we will keep the standards
new
/create
methods. - To keep it simple, a User can't edit or destroy a comment (it would be the same logic as for posts).
$ rails generate controller Comments index new create
As the comments are very dependent on their relative post, we can define the routes as nested resources:
# config/routes.rb
Rails.application.routes.draw do
resources :posts do
resources :comments, only: [:index, :new, :create]
end
end
This will add 3 new routes:
$ rake routes
# Prefix Verb URI Pattern Controller#Action
# post_comments GET /posts/:post_id/comments(.:format) comments#index
# POST /posts/:post_id/comments(.:format) comments#create
# new_post_comment GET /posts/:post_id/comments/new(.:format) comments#new
# [...]
We can see that those controller methods are now expecting a :post_id
parameter. Let's complete the CommentsController:
# app/controllers/comments_controller.rb
class CommentsController < ApplicationController
before_action :find_post
def index
@comments = @post.comments
end
def new
@comment = Comment.new
end
def create
@comment = @post.comments.build(comment_params)
if @comment.save
redirect_to post_path(@post)
else
render :new
end
end
private
def comment_params
params.require(:comment).permit(:content)
end
def find_post
@post = Post.find(params[:post_id])
end
end
As said earlier, we will use Rails to render JSON files and not HTML. However, for the purpose of this demo, let's stick with HTML views, so that we can have a visual demo of our blog. Let's update the views:
<!-- app/views/pages/home.html.erb -->
<h1>Home</h1>
<%= link_to "All the posts", posts_path %>
<!-- app/views/posts/index.html.erb -->
<h1>Posts</h1>
<% @posts.each do |post| %>
<h2><%= link_to post.title, post %></h2>
<p><strong>By: <%= post.author_name %></strong></p>
<p><%= post.content %></p>
<% end %>
<%= link_to "New", new_post_path %>
<!-- app/views/posts/show.html.erb -->
<h1><%= @post.title %></h1>
<p><strong>By: <%= @post.author_name %></strong></p>
<p><%= @post.content %></p>
<p><%= link_to "See Comments", post_comments_path(@post) %></p>
<%= link_to "All the posts", posts_path %> |
<%= link_to "Edit", edit_post_path(@post) %> |
<%= link_to "Destroy", post_path(@post), method: :delete, data: { confirm: "Are you sure?" } %>
<!-- app/views/posts/new.html.erb -->
<h1>New Post</h1>
<%= form_for(@post) do |f| %>
<%= f.label :title %><br>
<%= f.text_field :title %><br>
<%= f.label :author_name %><br>
<%= f.text_field :author_name %><br>
<%= f.label :content %><br>
<%= f.text_area :content %><br>
<%= f.submit %>
<% end %>
<!-- app/views/posts/edit.html.erb -->
<h1>Edit Post</h1>
<%= form_for(@post) do |f| %>
<%= f.label :title %><br>
<%= f.text_field :title %><br>
<%= f.label :author_name %><br>
<%= f.text_field :author_name %><br>
<%= f.label :content %><br>
<%= f.text_area :content %><br>
<%= f.submit %>
<% end %>
<!-- app/views/comments/index.html.erb -->
<h1>Comments for <%= @post.title %></h1>
<ul>
<% @comments.each do |comment| %>
<li><%= comment.content %></li>
<% end %>
</ul>
<%= link_to "New", new_post_comment_path(@post) %>
<!-- app/views/comments/new.html.erb -->
<h1>New Comment for <%= @post.title %></h1>
<%= form_for([@post, @comment]) do |f| %>
<%= f.label :content %><br>
<%= f.text_area :content %><br>
<%= f.submit %>
<% end %>
You can read the Documentation here and here to figure out how to use view helpers like form helpers, link helpers, etc.