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
# 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
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?
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
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.