Remember the story of tech leading a project in fours acts? It was a motivational (hopefully!) story about delivering a project and my experience with being a tech lead.
It was focused around repositories helping us move all ActiveRecord calls into single files (per model or context).
Although I provided some insights on what we did code-wise, I feel like it lacked a FULL technical explanation how it helped exactly. And it bore hard on me. After all, I’m a developer and I couldn’t let that knowledge-sharing opportunity slip away. Besides, looking back, I’ve noticed a couple of shortcomings in the presented solution. So, let me update the story with some tech meat and guide you through the process of creating a repository pattern using ActiveRecord.
Intro to Ruby on Rails Repositories
Don’t worry just yet, the solution I provided you with wasn’t bad, it simply wasn’t perfect—but in the end, what is?
For the sake of clarity, we wanted to achieve database calls separate from business logic. A nice, descriptive, and easily testable interface for querying and persistent data. Thin models and controllers. Single responsibilities. Separation of concerns. All the good stuff. But we were short of time and I was new to the topic—I have just learned about the repositories.
Let’s start by laying out what repositories are. I could paste several definitions here, but as I’m a visual-oriented person, like most humans, here’s an awesome graph that illustrates the idea:
Source: https://msdn.microsoft.com/en-us/library/ff649690.aspx
Moving on. Remember those?
class Contact < ActiveRecord::Base
after_destroy :do_something
belongs_to :user
belongs_to :category
has_many :emails, dependent: :destroy
validates :name, presence: true
validate :some_custom_validator
scope(:eligible_for_email, lambda do |user_id|
joins(:category)
.select("contacts.*, category.title AS category_title")
.where(emailed: false, unsubscribed: false, user_id: user_id)
end)
def status
emails.any? ? "Emailed" : "Never emailed"
end
private
def do_something
# Harmless callback
end
def some_custom_validator
# Can I even save?
end
end
Are you able to count the number of things this class is responsible for? Me neither. And that’s without mentioning all the ActiveRecord methods it inherits. Plus, the example is thinned, mind you. I saw and wrote Models 3-4 times as long and bloated. When business logic gets more and more complicated, things get increasingly unreadable and unmaintainable. Even though ActiveRecord has its place, we wanted something more flexible, with more explicit control over it.
But since the application was already written using AR, we didn’t want to go all in but rather take iterative steps—find all database calls, extract them, test them, remove AR.
RoR Repository Model with ActiveRecord
Is that even possible? Well, that’s the thing. This is not a perfect solution but: it works, gets the job done, meets our needs, leaves doors open for improvement. This is definitely not a definition of done—we meant to push the re-work further but life happened.
We took EVERY call to the database from the app and moved it to the repository. Here’s the example repository file:
class ContactRepository
class << self
def find_by(*attrs)
Contact.find_by(*attrs)
end
def destroy(contact)
contact.destroy
end
def eligible_for_email(user_id)
Contact
.joins(:category)
.select("contacts.*, category.title AS category_title")
.where(emailed: false, unsubscribed: false, user_id: user_id)
end
end
end
Here’s how we use this in the business logic code:
class SendEmail
def initialize(current_user_id, repo = ContactRepository)
@contacts_to_email = repo.eligible_for_email(current_user_id)
end
def call
# Do something with @contacts_to_email
end
end
module Api
module V1
class ContactsController < ApiController
def destroy
contact = ContactRepository.find_by(id: params[:id])
ContactRepository.destroy(contact)
head 204
end
end
end
end
You get the idea. We created a repo class for every model that required any database (ActiveRecord) call (querying or persisting).
The Good Part
- Database calls separate from business logic? Check.
Database-specific code is only in the repos themselves and we use them EVERYWHERE.
- Nice, descriptive, and easily testable interface? Check.
You look at the repo and you instantly know what it does. You read the methods, you know what you’ll get. Additionally, we can mock up repositories in all of the unit tests. Things got fast! No more unit-sorta-integration tests.
- Thin models and controllers? Single responsibilities? Separation of concerns? Check, check, and check.
Models are no longer responsible for communication with the database. Controllers listen to and respond to clients. Repositories query and persist data. Everything has its place. We achieved what we wanted to.
The Not-So-Good Part
The weak part that popped up during the in-house review of the previous story was that this is not a “by the book” repository pattern. Only the repositories should be responsible and have the ability to communicate with the database. Here, it’s still an AR Model thing; our repos are just an another abstraction layer on top of that.
So it might be tempting to just go “Ah, well, what’s the difference” and use Model methods instead. So, going with ActiveRecord is actually using the wrong tool for the job which necessitates putting trust in the developers’ discipline to use our repos.
The Best Part
However, it can always get better. The first steps for improvement that come to mind include:
1. Divide repos into queries and commands.
They can grow, but separation like this makes them even more descriptive and clean. It’s not always needed of course, everything should be suited to your needs.
2. Build your own entities.
Relying on AR models is risky, as I already mentioned above. In “by the book repository patterns,” repos should be the only place where database calls are performed. This is where entities can come in handy—they just describe how the object should look, no additional magicky things inside. Take a look at dry-rb gems or… simple struct.
3. Get rid of ActiveRecord completely.
For someone who got into Web development with all of Rails’ nice conventions, this is heavy. But the more I code, the more control I want. It’s like riding a bicycle with training wheels—easy to learn, accessible entry threshold, convenient, but if you want to start doing fancy tricks, well…
I decided to shift away from ActiveRecord (both the tool and the pattern) and use something more flexible due to working on more complicated projects with more business logic. Maybe Sequel instead? Or even the whole ROM? Still, I’m not saying ActiveRecord is bad or anything—just pick the right tools for the job!
We’ve come to an end of the repository pattern story. I’ll try to make my repos and my app a little better following these steps and come back to you with the results. As this is just one of the phases, I’m far from done yet. The solution outlined above would work, but I still wouldn’t risk leaving it like that in a project—too many loose ends. So stay tuned for the next part!