Skip to content

Ruby on Rails

Web & HTTPWebRuby

What it is

Rails is a full-stack web framework covering routing, ORM, migrations, background jobs, mailers, caching and testing, built on convention over configuration.

Models, views and controllers follow naming conventions that wire themselves together. ActiveRecord derives columns from the schema, so there is no mapping to maintain.

Installation

gem install rails

Getting started

The smallest useful thing you can do with it, and what each part means.

Model, scopes and validations
class Book < ApplicationRecord
  belongs_to :author
  has_many :reviews, dependent: :destroy

  validates :title, presence: true, length: { maximum: 200 }
  validates :isbn, uniqueness: true, allow_nil: true

  scope :recent, -> { where(published_at: 1.year.ago..) }
  scope :highly_rated, -> {
    joins(:reviews).group(:id).having('AVG(reviews.rating) > 4')
  }

  def average_rating = reviews.average(:rating)&.round(1)
end

# Scopes compose into one lazy query.
Book.recent.highly_rated.includes(:author).limit(10)
includes is what prevents the N+1 problem — without it, a view that renders book.author for twenty books issues twenty-one queries.
Controller and strong parameters
class BooksController < ApplicationController
  before_action :set_book, only: %i[show update destroy]

  def index
    @books = Book.includes(:author).recent.page(params[:page])
  end

  def create
    @book = Book.new(book_params)
    if @book.save
      render json: @book, status: :created
    else
      render json: { errors: @book.errors }, status: :unprocessable_entity
    end
  end

  private

  def set_book = @book = Book.find(params[:id])   # raises 404 if missing

  def book_params
    params.require(:book).permit(:title, :year, :author_id)
  end
end
Strong parameters are not optional: permitting the whole params hash would let a request set any column, including ones like admin flags.

Advanced usage

Where the library earns its place over a simpler alternative.

Background jobs and transactions
class ImportBooksJob < ApplicationJob
  queue_as :imports
  retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 5
  discard_on ActiveRecord::RecordNotFound

  def perform(import_id)   # pass the id, never the record
    import = Import.find(import_id)
    Import.transaction do
      import.rows.each { |row| Book.create!(row.attributes) }
      import.update!(status: :complete)
    end
  end
end

# Enqueue only after the surrounding transaction commits.
Book.transaction do
  book.save!
  NotifySubscribersJob.perform_later(book.id)
end
Passing an id rather than a record avoids a stale serialised object, and discard_on stops a job retrying forever over a record that has been deleted.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

ActiveRecord::RecordNotFound
find raises when nothing matches, which Rails renders as 404. Use find_by if nil is an acceptable outcome.
The page is slow and issues hundreds of queries
N+1 from a missing includes. Bullet will identify the exact association.

Best practices

  • Use includes or preload for any association a view touches; add the Bullet gem to catch N+1 in development.
  • Pass record ids to jobs, not the records themselves.
  • Keep controllers thin — extract logic into service, form or query objects before models reach a thousand lines.
  • Never skip strong parameters; permitting everything is a mass-assignment vulnerability.

Background

Why it exists, and what it was reacting to.

Extracted from Basecamp by David Heinemeier Hansson in 2004, Rails made database-backed web applications dramatically faster to build. Its conventions were copied into nearly every other ecosystem.