A big subset of smells in Rails apps is found in Active Record classes. As seen previously, these objects just do way too much by default and are abused to operate both persistence and business logic roles. One of the goals of a sustainable Rails architecture is to isolate Active Record down to its basic database-related capabilities, and to keep the app’s core business logic as decoupled as possible from it. To this end, a number of objects will be introduced on top of the persistence layer, starting with Repositories.
Repositories are responsible for the persistence layer of the app. They encapsulate Rails’ Active Record in a subset of simple methods for querying and persistence of data, and return simple read-only objects as a result. This allows the app to isolate Active Record only to this subset, exposing only the desired queries and methods to other layers through Repositories. Let’s refactor the previous example of the Blog app to encapsulate the Article Record behind a Repository.
As mentioned previously, Active Record objects are now referred to as simply
Records. The previous Article class is now moved from app/models
to
app/records
and renamed to Article Record.
# app/records/article_record.rb
class ArticleRecord < ApplicationRecord
self.table_name = 'articles'
validates :title, presence: true
validates :body, presence: true, length: { minimum: 10 }
end
All operations previously handled by Article Record, such as finding, creating, and deleting records are now done through the Article Repository.
# app/repositories/articles_repository.rb
class ArticleRepository
def all
ArticleRecord.all
end
def create(title:, body:)
ArticleRecord.create!(title: title, body: body)
end
def find(id)
ArticleRecord.find(id)
end
def update(id, title:, body:)
record = find(id)
record.update!(title: title, body: body)
record
end
def delete(id)
record = find(id)
record.destroy!
end
end
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
# GET /articles
def index
@articles = ArticleRepository.new.all
end
# GET /articles/1
def show
@article = ArticleRepository.new.find(params[:id])
end
# GET /articles/new
def new
@article = ArticleRecord.new
end
# GET /articles/1/edit
def edit
@article = ArticleRepository.new.find(params[:id])
end
# POST /articles
def create
@article = ArticleRepository.new.create(
title: article_params[:title], body: article_params[:body]
)
redirect_to article_path(@article), notice: 'Article was successfully created.'
rescue ActiveRecord::RecordInvalid => error
@article = error.record
render :new
end
# PATCH/PUT /articles/1
def update
@article = ArticleRepository.new.update(
params[:id], title: article_params[:title], body: article_params[:body]
)
redirect_to article_path(@article), notice: 'Article was successfully updated.'
rescue ActiveRecord::RecordInvalid => error
@article = error.record
render :edit
end
# DELETE /articles/1
def destroy
ArticleRepository.new.delete(params[:id])
redirect_to articles_url, notice: 'Article was successfully destroyed.'
end
private
# Only allow a list of trusted parameters through.
def article_params
params.require(:article_record).permit(:title, :body)
end
end
Note, however, that Active Record is not completely encapsulated just yet. After all, queries still return Record objects that controllers and views rely on in order to handle parameters and read data. These Records are used for multiple responsibilities: in some actions, such as new and edit, they represent the user’s input; in others, like in index and show, they play the role of actual persisted entities of the system. Records also hold the validation errors that might happen when a persistence operation is attempted.
In order to isolate Active Record completely, we must replace these cases with simpler objects for each of these responsibilities. Enter Inputs and Models.