Ruby on Rails Best Practices

Some Do’s and Don’ts to keep in mind while coding in ruby on rails

1. Fat Model, Skinny Controller

It is a commonly used phrase when talking about rails best practices.It basically means placing most of the business logic, data manipulation, and validations within the models (fat models) while keeping the controllers focused on handling request/response and routing (skinny controllers).

Your model might look something like this:

# Model: Post.rb
class Post < ApplicationRecord
  belongs_to :user
  has_many :comments
  has_many :likes
  has_and_belongs_to_many :categories

  # Business logic for creating a post
  def self.create_post(user, title, content, category_ids)
    post = user.posts.build(title: title, content: content)
    post.categories << Category.where(id: category_ids)
    post.save
    post
  end

  # Business logic for calculating the total likes on a post
  def total_likes
    likes.count
  end
end

While your controller might look like:

# Controller: PostsController.rb
class PostsController < ApplicationController
  def create
    post = Post.create_post(current_user, params[:title], params[:content], params[:category_ids])
    # handle success or error response
  end

  def show
    @post = Post.find(params[:id])
    @total_likes = @post.total_likes
  end
end

This can lead to more efficient and DRY (Don’t Repeat Yourself) code.

Moreover, by separating concerns and adhering to the Single Responsibility Principle (SRP), you make your codebase more modular and easier to maintain.

2. Eager Loading, when to and when not to use.

N+1 Problem is an infamous problem faced by many ORM using frameworks including rails.

It arises when an application makes N additional queries to the database for a collection of records, where N is the number of initial records are retrieved.

One solution to N+1 problem is Eager Loading, it uses a join or preload query to load associated records or data in advance, reducing the number of database queries and improving performance.

In Rails, eager loading can be accomplished using the includes method or the preload method.

Note that includes and preload have slightly different behaviors. includes performs a left outer join to load the associated data, which can help avoid the N+1 query problem. On the other hand, preload performs separate queries for each association and then associates the data in memory.

@users = User.includes(:posts).all

This way if you want to parse all Post for each User, rails will use 2 queries (1 for user, 1 for posts) instead of calling additional query to fetch Post for each User.

You can also use nested eager loading to fetch associations that have their own associations.

@products = Product.includes([:taxons, { variants: [:images]}, stock_items: :stock_location]).all

However, you should be aware of what all should you eager load before including everything for a query especially for large dataset or multiple associated models.

# Controller: ProductsController.rb
def index
  @products = Product.includes([:option_types, :taxons, { variants: [:prices, :images]}, product_properties: :property])
end

# View: products/index.html.erb
<% @products.each do |product| %>
  <h3><%= product.name %></h3>
  <% product.variants.each do |variant| %>
    <p><%= variant.title %></p>
    <p><%= variant.price.to_html %></p>
  <% end %>
  <% product.taxons.each do |taxon| %>
    <p><%= taxon.type %></p>
  <% end %>
<% end %>

Here, we have eager loaded option_types, product_properties as well as images for Products, which has never been used.

Therefore, by carefully considering your specific use case and evaluating the performance impact, you can decide whether or not to use eager loading.

3. Model Validations vs Database Validations

Rails has a powerful ORM(Object Relational Model) which it provides through ActiveRecord.Active Record includes validation features so data offered to the database can be validated before being accepted and persisted.

Validations help maintain data integrity and prevent the storage of invalid or inconsistent data in your application’s database. To perform model validation in Rails, you typically define validation rules within your model classes using the validates method.

# Model: User.rb
class User < ApplicationRecord
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }, uniqueness: true
  validates :password, presence: true, length: { minimum: 6 }
  validates :points presence: true, numericality: { greater_than_or_equal_to: 0 }
end

Whereas Database validations are implemented using constraints defined at the database schema level. These validations are enforced by the database when attempting to save data. In Rails you can add database validations through migrations.

def change
  create_table :users do |t|
    t.string :name, :null => false
    t.decimal :points, precision: 8, scale: 2, default: "0.0", null: false
  end
end

So the question arises which is better Model Validations or Database Validations?

Adding validations in the model saves you a database query (possibly across the network) that will essentially error out, and doing it in the database guarantees data consistency.

IMO simple, data-related validation (such as field constraints) should be done in the database. Any validation that is following some business rules (such as email validation) should be done in the model.

4. Prevent SQL Injection

SQLi (SQL Injection) is a type of security vulnerability that occurs when an application fails to properly sanitize or validate user-supplied input before incorporating it into SQL queries.

Since database is the core of any application, vulnerable sql queries can be very harmful for your application.

# Bad Practice
User.where("name = '#{params[:name]'") # SQL Injection!

If this particular line of code invoked with name = ‘fff’, the resulting query will be:

SELECT "users".* FROM "users" WHERE (name = 'fff')
 => #<ActiveRecord::Relation []>

But if it is set to “''OR 1='1'“:

SELECT "users".* FROM "users" WHERE (name = ' ' OR '1'='1')
=> #<ActiveRecord::Relation [#<User id: 1, name:'jack', …….>]>

Instead use dynamic attribute based finder

# Good Practice
User.find_by_name(name) # dynamic finder

5. Use Enums

An enum in Rails is a data type that represents a set of named values. The values of an enum are stored as integers in the database, but they can be accessed by name in Ruby code. This makes it easier to work with enums in your Rails application.

Add an enum to an existing model is by adding an integer column to that table.

class AddStatusToOrders < ActiveRecord::Migration[6.0]
  def change
    add_column :orders, :status, :integer, default: 0
  end
end

Then in your model you can define the value of integers for that column, you can add enum for statuslike given below

# Model: Order.rb
class Order < ApplicationRecord
  enum status: {
    placed: 0,
    packed: 1,
    shipped: 2,
    delivered: 3
  }
end

Once you have defined an enum, you can use it in your Ruby code like this:

order = Order.create(status: :placed)

# Check the status of the order
order.status == :placed
# or
order.placed?

# Change the status of the order
order.status = :packed
# or
order.packed!

# Scope
order.shipped # use instead Order.where(status: 'shipped')

You can use _prefix (or suffix) in enum definition, so that all the helpers can be prefixed(or suffixed) by the column name

# Model: Order.rb
enum status: {
    placed: 0,
    packed: 1,
    shipped: 2
}, _prefix: true

order.status_placed? # status == 'placed'
order.status_packed! # update(status: :packed)
order.status_shipped # User.where(status: :shipped)

6. Filters in controller

Filters are methods that are run before a controller action is executed.They can be used to perform common tasks, such as checking for authorization, setting up data, or redirecting the user to a different page.

class PostsController < ApplicationController
  before_action :authorize_user
  before_action :set_post, only: %w[show, update, delete]

  def index
    # Only authorized users can see the index page
  end

  def show
    # Set up the data for the show page
  end
...

  private

  def set_post
    @post = Post.find(params[:id])
  end
end

The authorize_user method is a private method that checks if the current user is authorized to access the index page. If the user is not authorized, the method redirects the user to the login page.

The set_post method is a private method that sets the @post instance variable to the post with the given ID. This variable is then available in the show, update and destroy action.

Hence, Filters are a powerful tool that can be used to improve the security and usability of your Rails application. They can also be used to simplify your code by centralizing common tasks.

7. Use ‘Time.current’ instead of ‘Time.now’

But keep in mind that Time.now returns a Time object representing the current time in the default system time zone. To get the current time in application’s configured time zone you have to use Time.zone.now

Unlike Time.now, which uses the default system time zone, Time.current takes into account the time zone configured for the Rails application. This ensures consistent and accurate time representation throughout the application, regardless of the server’s time zone settings.

# config/application.rb
config.time_zone = "Africa/Nairobi"

# Fetch current time in default system's zone
current_time = Time.now # 2023-07-13 10:57:05.751843358 +0530

# Fetch current time in application set time zone
current_time = Time.zone.now # Thu, 13 Jul 2023 08:27:07.607994466 EAT +03:00
# or better
current_time = Time.current # Thu, 13 Jul 2023 08:27:10.015227438 EAT +03:00

8. Concerns, Services and Helpers

All Concerns, Services and Helpers are used to create reusable code for your models, controllers and views respectively.

Concerns

In Rails, Concerns are modules that encapsulate reusable code and behavior that can be included in multiple classes or modules. It extends ActiveSupport::Concern module.

They help keep models focused and avoid excessive code duplication. Concerns are implemented using Ruby modules and included in classes using the include keyword.

# app/models/concerns/trashable.rb
module Trashable
  extend ActiveSupport::ConConcerncern

  included do
    scope :existing, -> { where(trashed: false) }
    scope :trashed, -> { where(trashed: true) }
  end

  def trash
    update_attribute :trashed, true
  end
end

# Model song.rb
class Song < ApplicationRecord
  include Trashable

  has_many :authors

  # ...
end

# Model album.rb
class Album < ApplicationRecord
  include Trashable
 
  has_many :authors
 
  def featured_authors
    authors.where(featured: true)
  end
 
  # ...
end

In the example above, the Trashable concern defines shared functionality related to trash entries. It is then included in the Songand Album model, allowing the model to leverage the methods defined within the concern.

Services

As your application grows, you may begin to see domain/business logic littered across the models and the controller. Such logics do not belong to either the controller or the model, so they make the code difficult to re-use and maintain.

Service objects are plain old Ruby objects (PORO’s) that do one thing .They encapsulate a set of business logic, moving it out of models and controllers and into a more focused setting.

# app/services/create_user_service.rb

class CreateUserService
  attr_reader :name, :email, :pass

  def initialize(name, email, pass)
    @name = name
    @email = email
    @pass = pass
  end

  def call
    prev_user = User.find_by email: @email
    raise StandardError, 'User already exists' if prev_user.present?

    User.create(name: @name, email: @email, pass: @pass)
  end
end


# Controller: users_controller.rb

class UserController < ApplicationController
    def create
      begin
        ActiveRecord::Base.transaction do
          # Create User
          user = CreateUserService.new(params[:name], params[:email], params[:pass]).call
          render json: { user: user }
        end
      rescue StandardError => e
        render json: { error: e }
      end
    end
  end

In above example, you can see we have a Create User service which creates and returns user with given parameters. This can be then reused in every controller where we need to create a new user.

Helpers

A helper is a method that is used in your Rails views to share reusable code. Helper methods are defined within modules called Helper Moduleand are automatically made available to the corresponding views.

# app/helpers/users_helper.rb

module UsersHelper
  def full_name(user)
    "#{user.first_name} #{user.last_name}"
  end
end

# View: users/show.html.erb

<h1><%= full_name(@user) %></h1>

The above example demonstrates how the full_name helper method can be used to generate and display a user’s full name within the view. It can be used wherever we need to display user’s full name.

Helpful Gems to make your life easier

ojLibrary for both parsing and generating JSON with a ton of options.

rack-mini-profilerMiddleware that provides performance profiling and diagnostics for Rack-based applications.

pryDebug APIs in real time.

rubocopStatic code analyzer and code formatter enforcing Ruby code style conventions.

bulletHelps detecting and alerts N+1 database query issues in Rails applications.

deviseA flexible and secure authentication solution for Ruby on Rails.

cancancanAuthorization library that restricts user access based on user roles and permissions.

annotateAutomatically adds schema information as comments to your models and specs.

discardSoft-delete implementation for ActiveRecord models.

sidekiq: Simple and efficient background job processing for Ruby.

friendlyId: Gem for creating human-readable URLs by using slugs for ActiveRecord models.

paperclip: Gem for handling file attachments in Rails applications.

In conclusion, embracing these practices will not only enhance the performance and scalability of your Rails applications but also contribute to a more enjoyable and productive development experience.

Stay committed to continuous learning and improvement, and let these best practices guide you towards building high-quality Rails applications. Happy coding!

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.