What's the better repo-approach? General repo-methods vs. specific repo-methods

Hey,
I’m very interested about your opinions about following 2 repo approaches:

1. Approach (general methods)

# users_repo.rb
class UsersRepo
  def find(id, **opts)
    get_relation(opts).by_pk(id).one!
  end

  private
  
  def get_relation(include: [])
    users.combine(include)
  end
end
# interactor 1
users_repo.find(1, include: [:posts, :department, :state])
users.filtered(state: :new, include: [:creator])

# interactor 2
users_repo.find(1, include: [:state])
users.filtered(deleted: true, include: [:deleter])

2. Approach (specific methods)

# users_repo.rb
class UsersRepo
  def find_with_posts_department(id)
    find(id, include: [:posts, :state])
  end

  def find_with_state(id)
    find(id, include: [:state])
  end

  private

  def find(id, **opts)
    get_relation(opts).by_pk(id).one!
  end
  
  def get_relation(include: [])
    users.combine(include)
  end
end
# interactor 1
users_repo.find_with_posts_department(1)
users.all_new_users

# interactor 2
users_repo.find_with_state(1)
users_repo.all_deleted

That’s just simplified code, I hope you know what I mean. Same applies to more complex filter-methods. I think the second approach is the better one for the long run. It keeps persistence-specific logic away from the calling logic. The first approach feels nice at first, because all use-cases can be served with one filtered or find method. Which approach do you use?

Thanks and best regards

I’ve settled on something very similar. I view the Repo/Relation boundary as reflecting the App/DB boundary, and so my Repo methods model business logic, and anything DB-specific is either hidden within its implementation, or exists on the Relation instead.

Where possible, my methods accept a struct as input rather than bare attributes.

Here’s an example Repo from my production code that retrieves and stores X.509 Certificates

class Repo < Repository[:certificates]
  commands :create, delete: :by_kid

  def find(kid)
    root.by_kid(kid).one!
  end

  def store_cert(kid, certificate)
    root.by_kid(kid).one or create(
      kid: kid, der: certificate.to_der, not_after: certificate.not_after
    )
  end
end
1 Like

Thank you for the insight. So you’re more likely to follow approach 2 (business specific repo methods). Do you have any arguments for approach 1?

I recommend against 1 because the more complex your interface is, the harder it becomes to test. This is one of the core problems with ActiveRecord; the API is so complex you end up having to use factories for everything, which slows down your test suite significantly.

I also recommend against this because it leads you to think of one repo per data type. You should be thinking of your persistence interfaces as implementing a use-case rather than an abstract data type. Another way ActiveRecord gets you in trouble is centralizing all logic for a table into one place, which overloads its responsibility.

So I favor a simple Repo class with simple methods that are clear, and serve a particular use-case. If you need something broadly reusable, think about writing modules or new Command classes etc that can be shared by multiple Repos.

1 Like