What is the repository pattern? (In simple terms)
In the famous book "Patterns of Enterprise Application Architecture", Martin Fowler define a repository with the following phrase
A repository performs the tasks of an intermediary between the domain model layer and data mapping.
I'm a visual person(hope you're too) so this quote can be described as the follow diagram:
Model(or Entity) on this diagram just represents the structure of an actor(user for example) and the whole database communication is handled by the repository class, that way we can easily swap repositories that use different databases or even mock the whole repository to isolate on unit tests.
Applying to rails
Unfortunately in rails we can't go 100% by the book on this pattern so we need to understand the concept and adapt to our current situation. Let's understand the general concepts and the limitations we have with ActiveRecord on rails today
The general concept
The general concept around repository pattern is to decouple the database communication to a new class, allowing easy mocking and swapping of implementations(use a different class for the same entity with the same methods_names/types but with different database for example).
Our limitation
Well, what about the limitations? basically it's around the fact that rails is a MVC framework that uses convention over configuration a lot, so we can't delegate database functionality without calling the model
The solution
To solve this we'll rely on good professional decision (it is what it is) to keep the model without any decision method, just configuration and field declaration.
Let's consider a sample rails app with a Task
model like the following:
class Task < ApplicationRecord
end
For this model we can define a TaskRepository
located at app/repositories/task_repository.rb
like the following:
class TaskRepository
def initialize(db = Todo)
@db = db
end
def find_by_id(id:)
@db.find(id)
end
def list_all
@db.all
end
end
After defining this class we can use it at the controller (or a service if you want to decouple even more) like the following:
Observe that we receive the repository on the constructor, so we can replace if needed.
class TaskController < ApplicationController
def initialize(repository = TaskRepository.new)
@repo = repository
end
def index
@tasks = @repo.list_all
end
def show
@task = @repo.find_by_id(id: params[:id])
end
end
Nice right? That way we have a couple advantages:
More control about the methods and what it receives (by using named params)
Ability to swap the repositories (just change the initialize on controller to a different class).
For example we could define a similar repository implementing a different library or database(like scyllaDB 👀)
- Easy mocking (let's see this next).
Testing/Mocking
Testing is a very important aspect of development using RoR and to keep the unit tests offline and without any external dependencies we need to rely on mocking, the principal advantage of the repository pattern is to make our life easier while mocking so let's see how we do it with a simple test example:
class MockedTaskRepository
def list_all
['mocked']
end
end
describe TodoController do
let (:controller) { TodoController.new(MockedTaskRepository.new) }
it 'should return all tasks' do
tasks = controller.index
expect(tasks).to eq(['mocked'])
expect(tasks.length).to be(1)
end
end
See? we can easily define another class and reuse it on tests or any other context that we want.
Top comments (2)
Active Records is already an abstraction on top of the DB, it has a lot of more convenience methods because of the nature of its pattern, but, at a certain degree we could already treat our models as repositories.
IMHO, I would see more fit if we use another ORM library (like sqlize), because it is lacking a lot of stuff that ActiveRecord has.
Yeah, certainly! I would still use repository pattern on active record projects, just because it's easier to isolate behavior instead of using the "fat model" philosophy that we have been using.
That said, I agree that this pattern could be used with something like sqlize, sequel or ROM
Recently I had a use case at work that I needed to create a v2 route using a different database (MongoDB) and since I was already using repository pattern, it was just a matter of creating the new repository and copying the controller method (because all method names remain the same)