October 25, 2018
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 Repositories
But 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. And man, it’s a whole big concept.
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:
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.
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-specific code is only in the repos themselves and we use them EVERYWHERE.
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.
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.
However, it can always get better. The first steps for improvement that come to mind include:
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.
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.
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!