Starting with Ruby on Rails [v6]- Part 2

EC

Esteban Campos / November 05, 2020

10 min read––– views

As a continuation of the Starting with Ruby on Rails v6, we are going to keep using Learn Enough tutorial.

In this post, we’ll develop a demo application to show off some of the power of Rails. The purpose is to get a high-level overview of Ruby on Rails programming (and web development in general) by rapidly generating an application using scaffold generators, which create a large amount of functionality automatically. The resulting app will allow us to interact with it through its URLs, giving us insight into the structure of a Rails application, including a first example of the REST architecture favored by Rails.

We are going to use scaffolding, that way we can have a palpable sense of what Ruby on Rails can offer, but later we are going to cover, step by step what is happening behind the hood. At each stage of developing, we will write small, bite-sized pieces of code—simple enough to understand. The cumulative effect will be a deeper, more flexible knowledge of Rails, giving you a good background for writing nearly any type of web application.

Warning: Following the scaffolding approach risks turning you into a virtuoso script generator with little (and brittle) actual knowledge of Rails.

Table of Contents#

  1. Let's plan the dummy application
  2. Let's create some resources
  3. Using Rails console
  4. Conclusion
  5. Resources

1. Let's plan the dummy application#

1.1 Objective#

As with the forthcoming sample application, the app will consist of users and their associated microposts (thus constituting a minimalist Twitter-style app).

1.2 Setting up#

We are going to create a new app called toy_app

$ rails _6.0.3.4_ new toy_app
$ cd toy_app/

With the next fixed dependencies:

Gemfile
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby "2.6.6"
gem "rails", "6.0.3.4"
gem "puma", "4.3.6"
gem "sass-rails", "6.0.0"
gem "webpacker", "4.2.2"
gem "turbolinks", "5.2.1"
gem "jbuilder", "2.10.0"
gem "bootsnap", "1.4.6", require: false

group :development, :test do
  gem "sqlite3", "1.4.2"
  gem "byebug", "11.1.3", platforms: [:mri, :mingw, :x64_mingw]
end

group :development do
  gem "web-console", "4.0.2"
  gem "listen", "3.2.1"
  gem "spring", "2.1.1"
  gem "spring-watcher-listen", "2.0.1"
end

group :test do
  gem "capybara", "3.32.2"
  gem "selenium-webdriver", "3.142.7"
  gem "webdrivers", "4.3.0"
end

group :production do
  gem "pg", "1.2.3"
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
# Uncomment the following line if you're running Rails
# on a native Windows system:
# gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

1.3 Planning the models#

We are going to need users. We are going to keep it simple.

Users

PropertyType
idinteger
namestring
emailstring

Posts

The posts has only an id and a content field. Also, each post is associated with a particular user (user_id).

PropertyType
idinteger
contentstring
user_idstring

2. Let's create some resources#

2.1 User#

The syntax to scaffold our Users model with some optional parameters for the data model’s attributes:

rails generate scaffold User name:string email:string

By including name:string and email:string, we have arranged for the User model to have the desirable structure (Note that there is no need to include a parameter for id; it is created automatically by Rails for use as the primary key in the database.)

To proceed with the application, we first need to migrate the database using rails db:migrate.

$ rails db:migrate
== CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0027s
== CreateUsers: migrated (0.0036s) =============================

The effect of last command is to update the database with our new users data model.

2.1.1 Users routes#

The scaffolding process create a basic CRUD with the following routes:

URLActionPurpose
/usersindexpage to list all users
/users/1showpage to show user with id 1
/users/newnewpage to make a new user
/users/1/editeditpage to edit user with id 1

2.1.2 Users Controller#

A controller contains a collection of related actions, and the pages from 2.1.1 correspond to actions in the Users controller. The Users controller in schematic form.

app/controllers/users_controller.rb
class UsersController < ApplicationController

  def index
  end

  def show
  end

  def new
  end

  def edit
  end

  def create
  end

  def update
  end

  def destroy
  end
end

2.1.3 MVC in action#

Here is a summary of the steps shown in above figure:

  1. The browser issues a request for the /users URL.
  2. Rails routes /users to the index action in the Users controller.
  3. The index action asks the User model to retrieve all users (User.all).
  4. The User model pulls all the users from the database.
  5. The User model returns the list of users to the controller.
  6. The controller captures the users in the @users variable, which is passed to the index view.
  7. The view uses embedded Ruby to render the page as HTML.
  8. The controller passes the HTML back to the browser

You might notice that there are more actions than there are pages; the index, show, new, and edit actions all correspond to pages from Section 2.1.1, but there are additional create, update, and destroy actions as well. These actions don’t typically render pages (although they can); instead, their main purpose is to modify information about users in the database.

HTTP requestURLActionPurpose
GET/usersindexpage to list all users
GET/users/1showpage to show user with id 1
GET/users/newnewpage to make a new user
POST/userscreatecreate a new user
GET/users/1/editeditpage to edit user with id 1
PATCH/users/1updateupdate user with id 1
DELETE/users/1destroydelete user with id 1

Just to examine the relationship between the Users Controller and the User Model, let’s focus on the index action.

app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def index
    @users = User.all
  end
  .
  .
  .
end

This index action has the line @users = User.all which asks the User model to retrieve a list of all the users from the database and then places them in the variable @users. Once the @users variable is defined, the controller calls the view. Variables that start with the @sign, called instance variables, are automatically available in the views; in this case, the index.html.erb.

app/views/users/index.html.erb
<p id="notice"><%= notice %></p>

<h1>Users</h1>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Email</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @users.each do |user| %>
      <tr>
        <td><%= user.name %></td>
        <td><%= user.email %></td>
        <td><%= link_to 'Show', user %></td>
        <td><%= link_to 'Edit', edit_user_path(user) %></td>
        <td><%= link_to 'Destroy', user, method: :delete,
                         data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New User', new_user_path %>

The view converts its contents to HTML, which is then returned by the controller to the browser for display.

2.2. Microposts#

Any micropost worthy of the name should have some means of enforcing the length of the post. Implementing this constraint in Rails is easy with validations; to accept microposts with at most 140 characters, we use a length validation.

app/models/micropost.rb
class Micropost < ApplicationRecord
  validates :content, length: { maximum: 140 }
end

2.3 A user has_many microposts#

One of the most powerful features of Rails is the ability to form associations between different data models. In the case of our User model, each user potentially has many posts. We can express this in code by updating the User and Post models.

app/models/user.rb
class User < ApplicationRecord
  has_many :microposts
end
app/models/post.rb
class Micropost < ApplicationRecord
  belongs_to :user
  validates :content, length: { maximum: 140 }
end

There is built-in association with belongs_to, the model/controller automatically looks for the <TABLE_NAME>_id that is defined for that relationship, in our case, that means User and user_id.

3. Using Rails console#

We are far from have a fully functional app but we can still create some users and microposts. Also, we can interact directly with our database using rails commands:

$ rails console
>> first_user = User.first
   (0.5ms)  SELECT sqlite_version(*)
  User Load (0.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC
  LIMIT ?  [["LIMIT", 1]]
 => #<User id: 1, name: "Esteban Campos", email: "campos.esteban@gmail.com",
 created_at: "2020-10-08 09:39:14", updated_at: "2020-10-08 09:41:24">
>> first_user.microposts
  Micropost Load (3.2ms)  SELECT "microposts".* FROM "microposts" WHERE
  "microposts"."user_id" = ? LIMIT ?  [["user_id", 1], ["LIMIT", 11]]
 => #<ActiveRecord::Associations::CollectionProxy [#<Micropost id: 1, content:
 "First micropost!", user_id: 1, created_at: "2020-10-08 10:04:13", updated_at:
 "2020-10-08 10:04:13">, #<Micropost id: 2, content: "Second micropost",
 user_id: 1, created_at: "2020-10-08 09:40:14", updated_at: "2020-10-08
 10:04:30">]>
>> micropost = first_user.microposts.first
  Micropost Load (0.2ms)  SELECT "microposts".* FROM "microposts" WHERE
  "microposts"."user_id" = ? ORDER BY "microposts"."id" ASC LIMIT ?
  [["user_id", 1], ["LIMIT", 1]]
 => #<Micropost id: 1, content: "First micropost!", user_id: 1, created_at:
 "2020-10-08 10:04:13", updated_at: "2020-10-08 10:04:13">
>> micropost.user
 => #<User id: 1, name: "Esteban Campos", email: "campos.esteban@gmail.com",
 created_at: "2020-10-08 09:40:14", updated_at: "2020-10-08 09:41:24"
>> exit

In the above example, we queried for the first user: User.first and the value for that query was stored in first_user. Also, we could retrieve all the microposts related to that user, just calling first_user.microposts. This show the power of rails and how automatically can guess relationships between models. Later, we tried with first_user.microposts.first that returns the first micropost and we retrieved the user that belongs to that entry calling first_user.micropost.first.user.

4. Conclusion#

We’ve come now to the end of the high-level overview of a Rails application. The app described here has several strengths and a host of weaknesses.

You might use low code if...

  • High-level overview of Rails

  • Introduction to MVC

  • First taste of the REST architecture

  • Beginning data modeling

  • A live, database-backed web application in production

You might not use low code if...

  • No automatic user/micropost association

  • No notion of “following” or “followed”

  • No micropost feed

  • No meaningful tests

4.1 What we learned until now#

  • scaffolding automatically creates code to model data and interact with it through the web.
  • Scaffolding is good for getting started quickly but is bad for understanding.
  • Rails uses the Model-View-Controller (MVC) pattern for structuring web applications.
  • As interpreted by Rails, the REST architecture includes a standard set of URLs and controller actions for interacting with data models.
  • Rails supports data validations to place constraints on the values of data model attributes.
  • Rails comes with built-in functions for defining associations between different data models.
  • We can interact with Rails applications at the command line using the Rails console.

5. Resources#